mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-19 02:42:29 +08:00
fix: noisy debug logging + auto model routing in browse server
Server-side silent catch blocks (22 instances) now log with [browse] prefix: chat persistence, session save/load, agent kill, tab pin/restore, welcome page, buffer flush, worktree cleanup, lock files, SSE streams. Also adds pickSidebarModel() — routes sidebar messages to sonnet for navigation/interaction (click, goto, fill, screenshot) and opus for analysis/comprehension (summarize, describe, find bugs). Sonnet is ~4x faster for action commands with zero quality difference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,31 @@ function validateAuth(req: Request): boolean {
|
|||||||
return header === `Bearer ${AUTH_TOKEN}`;
|
return header === `Bearer ${AUTH_TOKEN}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Sidebar Model Router ────────────────────────────────────────
|
||||||
|
// Fast model for navigation/interaction, smart model for reading/analysis.
|
||||||
|
// The delta between sonnet and opus on "click @e24" is 5-10x in latency
|
||||||
|
// and cost, with zero quality difference. Save opus for when you need it.
|
||||||
|
|
||||||
|
const ANALYSIS_WORDS = /\b(what|why|how|explain|describe|summarize|analyze|compare|review|read\b.*\b(and|then)|tell\s*me|find.*bugs?|check.*for|assess|evaluate|report)\b/i;
|
||||||
|
const ACTION_PATTERNS = /^(go\s*to|open|navigate|click|tap|press|fill|type|enter|scroll|screenshot|snap|reload|refresh|back|forward|close|submit|select|toggle|expand|collapse|dismiss|accept|upload|download|focus|hover|cleanup|clean\s*up)\b/i;
|
||||||
|
const ACTION_ANYWHERE = /\b(go\s*to|click|tap|fill\s*(in|out)?|type\s*in|navigate\s*to|open\s*(the|this|that)?|take\s*a?\s*screenshot|scroll\s*(down|up|to)|reload|refresh|submit|press\s*(the|enter|button))\b/i;
|
||||||
|
|
||||||
|
function pickSidebarModel(message: string): string {
|
||||||
|
const msg = message.trim();
|
||||||
|
|
||||||
|
// Analysis/comprehension always gets opus — regardless of action verbs mixed in
|
||||||
|
if (ANALYSIS_WORDS.test(msg)) return 'opus';
|
||||||
|
|
||||||
|
// Short action commands (under ~80 chars, starts with an action verb)
|
||||||
|
if (msg.length < 80 && ACTION_PATTERNS.test(msg)) return 'sonnet';
|
||||||
|
|
||||||
|
// Longer messages that are clearly action-oriented (no analysis words already checked above)
|
||||||
|
if (ACTION_ANYWHERE.test(msg)) return 'sonnet';
|
||||||
|
|
||||||
|
// Everything else: multi-step, ambiguous, or complex
|
||||||
|
return 'opus';
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Help text (auto-generated from COMMAND_DESCRIPTIONS) ────────
|
// ─── Help text (auto-generated from COMMAND_DESCRIPTIONS) ────────
|
||||||
function generateHelpText(): string {
|
function generateHelpText(): string {
|
||||||
// Group commands by category
|
// Group commands by category
|
||||||
@@ -246,7 +271,9 @@ function addChatEntry(entry: Omit<ChatEntry, 'id'>, tabId?: number): ChatEntry {
|
|||||||
// Persist to disk (best-effort)
|
// Persist to disk (best-effort)
|
||||||
if (sidebarSession) {
|
if (sidebarSession) {
|
||||||
const chatFile = path.join(SESSIONS_DIR, sidebarSession.id, 'chat.jsonl');
|
const chatFile = path.join(SESSIONS_DIR, sidebarSession.id, 'chat.jsonl');
|
||||||
try { fs.appendFileSync(chatFile, JSON.stringify(full) + '\n'); } catch {}
|
try { fs.appendFileSync(chatFile, JSON.stringify(full) + '\n'); } catch (err: any) {
|
||||||
|
console.error('[browse] Failed to persist chat entry:', err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return full;
|
return full;
|
||||||
}
|
}
|
||||||
@@ -271,11 +298,17 @@ function loadSession(): SidebarSession | null {
|
|||||||
const chatFile = path.join(SESSIONS_DIR, session.id, 'chat.jsonl');
|
const chatFile = path.join(SESSIONS_DIR, session.id, 'chat.jsonl');
|
||||||
try {
|
try {
|
||||||
const lines = fs.readFileSync(chatFile, 'utf-8').split('\n').filter(Boolean);
|
const lines = fs.readFileSync(chatFile, 'utf-8').split('\n').filter(Boolean);
|
||||||
chatBuffer = lines.map(line => { try { return JSON.parse(line); } catch { return null; } }).filter(Boolean);
|
const parsed = lines.map(line => { try { return JSON.parse(line); } catch { return null; } });
|
||||||
|
const discarded = parsed.filter(x => x === null).length;
|
||||||
|
if (discarded > 0) console.warn(`[browse] Discarding ${discarded} corrupted chat entries during load`);
|
||||||
|
chatBuffer = parsed.filter(Boolean);
|
||||||
chatNextId = chatBuffer.length > 0 ? Math.max(...chatBuffer.map(e => e.id)) + 1 : 0;
|
chatNextId = chatBuffer.length > 0 ? Math.max(...chatBuffer.map(e => e.id)) + 1 : 0;
|
||||||
} catch {}
|
} catch (err: any) {
|
||||||
|
if (err.code !== 'ENOENT') console.warn('[browse] Chat history not loaded:', err.message);
|
||||||
|
}
|
||||||
return session;
|
return session;
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
|
if (err.code !== 'ENOENT') console.error('[browse] Failed to load session:', err.message);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,7 +336,9 @@ function createWorktree(sessionId: string): string | null {
|
|||||||
Bun.spawnSync(['git', 'worktree', 'remove', '--force', worktreeDir], {
|
Bun.spawnSync(['git', 'worktree', 'remove', '--force', worktreeDir], {
|
||||||
cwd: repoRoot, stdout: 'pipe', stderr: 'pipe', timeout: 5000,
|
cwd: repoRoot, stdout: 'pipe', stderr: 'pipe', timeout: 5000,
|
||||||
});
|
});
|
||||||
try { fs.rmSync(worktreeDir, { recursive: true, force: true }); } catch {}
|
try { fs.rmSync(worktreeDir, { recursive: true, force: true }); } catch (err: any) {
|
||||||
|
console.warn('[browse] Failed to clean stale worktree dir:', err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current branch/commit
|
// Get current branch/commit
|
||||||
@@ -343,8 +378,12 @@ function removeWorktree(worktreePath: string | null): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Cleanup dir if git worktree remove didn't
|
// Cleanup dir if git worktree remove didn't
|
||||||
try { fs.rmSync(worktreePath, { recursive: true, force: true }); } catch {}
|
try { fs.rmSync(worktreePath, { recursive: true, force: true }); } catch (err: any) {
|
||||||
} catch {}
|
console.warn('[browse] Failed to remove worktree dir:', worktreePath, err.message);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.warn('[browse] Worktree removal error:', err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSession(): SidebarSession {
|
function createSession(): SidebarSession {
|
||||||
@@ -372,7 +411,9 @@ function saveSession(): void {
|
|||||||
if (!sidebarSession) return;
|
if (!sidebarSession) return;
|
||||||
sidebarSession.lastActiveAt = new Date().toISOString();
|
sidebarSession.lastActiveAt = new Date().toISOString();
|
||||||
const sessionFile = path.join(SESSIONS_DIR, sidebarSession.id, 'session.json');
|
const sessionFile = path.join(SESSIONS_DIR, sidebarSession.id, 'session.json');
|
||||||
try { fs.writeFileSync(sessionFile, JSON.stringify(sidebarSession, null, 2)); } catch {}
|
try { fs.writeFileSync(sessionFile, JSON.stringify(sidebarSession, null, 2)); } catch (err: any) {
|
||||||
|
console.error('[browse] Failed to save session:', err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function listSessions(): Array<SidebarSession & { chatLines: number }> {
|
function listSessions(): Array<SidebarSession & { chatLines: number }> {
|
||||||
@@ -382,11 +423,16 @@ function listSessions(): Array<SidebarSession & { chatLines: number }> {
|
|||||||
try {
|
try {
|
||||||
const session = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, d, 'session.json'), 'utf-8'));
|
const session = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, d, 'session.json'), 'utf-8'));
|
||||||
let chatLines = 0;
|
let chatLines = 0;
|
||||||
try { chatLines = fs.readFileSync(path.join(SESSIONS_DIR, d, 'chat.jsonl'), 'utf-8').split('\n').filter(Boolean).length; } catch {}
|
try { chatLines = fs.readFileSync(path.join(SESSIONS_DIR, d, 'chat.jsonl'), 'utf-8').split('\n').filter(Boolean).length; } catch {
|
||||||
|
// Expected: no chat file yet
|
||||||
|
}
|
||||||
return { ...session, chatLines };
|
return { ...session, chatLines };
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
} catch { return []; }
|
} catch (err: any) {
|
||||||
|
console.warn('[browse] Failed to list sessions:', err.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processAgentEvent(event: any): void {
|
function processAgentEvent(event: any): void {
|
||||||
@@ -482,7 +528,14 @@ function spawnClaude(userMessage: string, extensionUrl?: string | null, forTabId
|
|||||||
const prompt = `${systemPrompt}\n\n<user-message>\n${escapedMessage}\n</user-message>`;
|
const prompt = `${systemPrompt}\n\n<user-message>\n${escapedMessage}\n</user-message>`;
|
||||||
// Never resume — each message is a fresh context. Resuming carries stale
|
// Never resume — each message is a fresh context. Resuming carries stale
|
||||||
// page URLs and old navigation state that makes the agent fight the user.
|
// page URLs and old navigation state that makes the agent fight the user.
|
||||||
const args = ['-p', prompt, '--model', 'opus', '--output-format', 'stream-json', '--verbose',
|
|
||||||
|
// Auto model routing: fast model for navigation/interaction, smart model for reading/analysis.
|
||||||
|
// Navigation, clicking, filling forms, screenshots = deterministic tool calls, no thinking needed.
|
||||||
|
// Reading, summarizing, analyzing, explaining = needs comprehension.
|
||||||
|
const model = pickSidebarModel(userMessage);
|
||||||
|
console.log(`[browse] Sidebar model: ${model} for "${userMessage.slice(0, 60)}"`);
|
||||||
|
|
||||||
|
const args = ['-p', prompt, '--model', model, '--output-format', 'stream-json', '--verbose',
|
||||||
'--allowedTools', 'Bash,Read,Glob,Grep'];
|
'--allowedTools', 'Bash,Read,Glob,Grep'];
|
||||||
|
|
||||||
addChatEntry({ ts: new Date().toISOString(), role: 'agent', type: 'agent_start' });
|
addChatEntry({ ts: new Date().toISOString(), role: 'agent', type: 'agent_start' });
|
||||||
@@ -521,8 +574,12 @@ function spawnClaude(userMessage: string, extensionUrl?: string | null, forTabId
|
|||||||
|
|
||||||
function killAgent(): void {
|
function killAgent(): void {
|
||||||
if (agentProcess) {
|
if (agentProcess) {
|
||||||
try { agentProcess.kill('SIGTERM'); } catch {}
|
try { agentProcess.kill('SIGTERM'); } catch (err: any) {
|
||||||
setTimeout(() => { try { agentProcess?.kill('SIGKILL'); } catch {} }, 3000);
|
console.warn('[browse] Failed to SIGTERM agent:', err.message);
|
||||||
|
}
|
||||||
|
setTimeout(() => { try { agentProcess?.kill('SIGKILL'); } catch (err: any) {
|
||||||
|
console.warn('[browse] Failed to SIGKILL agent:', err.message);
|
||||||
|
} }, 3000);
|
||||||
}
|
}
|
||||||
agentProcess = null;
|
agentProcess = null;
|
||||||
agentStartTime = null;
|
agentStartTime = null;
|
||||||
@@ -600,8 +657,8 @@ async function flushBuffers() {
|
|||||||
fs.appendFileSync(DIALOG_LOG_PATH, lines);
|
fs.appendFileSync(DIALOG_LOG_PATH, lines);
|
||||||
lastDialogFlushed = dialogBuffer.totalAdded;
|
lastDialogFlushed = dialogBuffer.totalAdded;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
// Flush failures are non-fatal — buffers are in memory
|
console.error('[browse] Buffer flush failed:', err.message);
|
||||||
} finally {
|
} finally {
|
||||||
flushInProgress = false;
|
flushInProgress = false;
|
||||||
}
|
}
|
||||||
@@ -639,7 +696,9 @@ const inspectorSubscribers = new Set<InspectorSubscriber>();
|
|||||||
function emitInspectorEvent(event: any): void {
|
function emitInspectorEvent(event: any): void {
|
||||||
for (const notify of inspectorSubscribers) {
|
for (const notify of inspectorSubscribers) {
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
try { notify(event); } catch {}
|
try { notify(event); } catch (err: any) {
|
||||||
|
console.error('[browse] Inspector event subscriber threw:', err.message);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -725,7 +784,9 @@ async function handleCommand(body: any): Promise<Response> {
|
|||||||
if (tabId !== undefined && tabId !== null) {
|
if (tabId !== undefined && tabId !== null) {
|
||||||
savedTabId = browserManager.getActiveTabId();
|
savedTabId = browserManager.getActiveTabId();
|
||||||
// bringToFront: false — internal tab pinning must NOT steal window focus
|
// bringToFront: false — internal tab pinning must NOT steal window focus
|
||||||
try { browserManager.switchTab(tabId, { bringToFront: false }); } catch {}
|
try { browserManager.switchTab(tabId, { bringToFront: false }); } catch (err: any) {
|
||||||
|
console.warn('[browse] Failed to pin tab', tabId, ':', err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block mutation commands while watching (read-only observation mode)
|
// Block mutation commands while watching (read-only observation mode)
|
||||||
@@ -809,7 +870,9 @@ async function handleCommand(body: any): Promise<Response> {
|
|||||||
browserManager.resetFailures();
|
browserManager.resetFailures();
|
||||||
// Restore original active tab if we pinned to a specific one
|
// Restore original active tab if we pinned to a specific one
|
||||||
if (savedTabId !== null) {
|
if (savedTabId !== null) {
|
||||||
try { browserManager.switchTab(savedTabId, { bringToFront: false }); } catch {}
|
try { browserManager.switchTab(savedTabId, { bringToFront: false }); } catch (restoreErr: any) {
|
||||||
|
console.warn('[browse] Failed to restore tab after command:', restoreErr.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new Response(result, {
|
return new Response(result, {
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -818,7 +881,9 @@ async function handleCommand(body: any): Promise<Response> {
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// Restore original active tab even on error
|
// Restore original active tab even on error
|
||||||
if (savedTabId !== null) {
|
if (savedTabId !== null) {
|
||||||
try { browserManager.switchTab(savedTabId, { bringToFront: false }); } catch {}
|
try { browserManager.switchTab(savedTabId, { bringToFront: false }); } catch (restoreErr: any) {
|
||||||
|
console.warn('[browse] Failed to restore tab after error:', restoreErr.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activity: emit command_end (error)
|
// Activity: emit command_end (error)
|
||||||
@@ -851,7 +916,9 @@ async function shutdown() {
|
|||||||
|
|
||||||
console.log('[browse] Shutting down...');
|
console.log('[browse] Shutting down...');
|
||||||
// Clean up CDP inspector sessions
|
// Clean up CDP inspector sessions
|
||||||
try { detachSession(); } catch {}
|
try { detachSession(); } catch (err: any) {
|
||||||
|
console.warn('[browse] Failed to detach CDP session:', err.message);
|
||||||
|
}
|
||||||
inspectorSubscribers.clear();
|
inspectorSubscribers.clear();
|
||||||
// Stop watch mode if active
|
// Stop watch mode if active
|
||||||
if (browserManager.isWatching()) browserManager.stopWatch();
|
if (browserManager.isWatching()) browserManager.stopWatch();
|
||||||
@@ -869,11 +936,15 @@ async function shutdown() {
|
|||||||
// Clean up Chromium profile locks (prevent SingletonLock on next launch)
|
// Clean up Chromium profile locks (prevent SingletonLock on next launch)
|
||||||
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
|
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
|
||||||
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
|
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
|
||||||
try { fs.unlinkSync(path.join(profileDir, lockFile)); } catch {}
|
try { fs.unlinkSync(path.join(profileDir, lockFile)); } catch (err: any) {
|
||||||
|
console.debug('[browse] Lock cleanup:', lockFile, err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up state file
|
// Clean up state file
|
||||||
try { fs.unlinkSync(config.stateFile); } catch {}
|
try { fs.unlinkSync(config.stateFile); } catch (err: any) {
|
||||||
|
console.debug('[browse] State file cleanup:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
@@ -885,7 +956,9 @@ process.on('SIGINT', shutdown);
|
|||||||
// Defense-in-depth — primary cleanup is the CLI's stale-state detection via health check.
|
// Defense-in-depth — primary cleanup is the CLI's stale-state detection via health check.
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
process.on('exit', () => {
|
process.on('exit', () => {
|
||||||
try { fs.unlinkSync(config.stateFile); } catch {}
|
try { fs.unlinkSync(config.stateFile); } catch {
|
||||||
|
// Best-effort on exit
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -894,15 +967,23 @@ function emergencyCleanup() {
|
|||||||
if (isShuttingDown) return;
|
if (isShuttingDown) return;
|
||||||
isShuttingDown = true;
|
isShuttingDown = true;
|
||||||
// Kill agent subprocess if running
|
// Kill agent subprocess if running
|
||||||
try { killAgent(); } catch {}
|
try { killAgent(); } catch (err: any) {
|
||||||
|
console.error('[browse] Emergency: failed to kill agent:', err.message);
|
||||||
|
}
|
||||||
// Save session state so chat history persists across crashes
|
// Save session state so chat history persists across crashes
|
||||||
try { saveSession(); } catch {}
|
try { saveSession(); } catch (err: any) {
|
||||||
|
console.error('[browse] Emergency: failed to save session:', err.message);
|
||||||
|
}
|
||||||
// Clean Chromium profile locks
|
// Clean Chromium profile locks
|
||||||
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
|
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
|
||||||
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
|
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
|
||||||
try { fs.unlinkSync(path.join(profileDir, lockFile)); } catch {}
|
try { fs.unlinkSync(path.join(profileDir, lockFile)); } catch (err: any) {
|
||||||
|
console.debug('[browse] Emergency lock cleanup:', lockFile, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try { fs.unlinkSync(config.stateFile); } catch (err: any) {
|
||||||
|
console.debug('[browse] Emergency state cleanup:', err.message);
|
||||||
}
|
}
|
||||||
try { fs.unlinkSync(config.stateFile); } catch {}
|
|
||||||
}
|
}
|
||||||
process.on('uncaughtException', (err) => {
|
process.on('uncaughtException', (err) => {
|
||||||
console.error('[browse] FATAL uncaught exception:', err.message);
|
console.error('[browse] FATAL uncaught exception:', err.message);
|
||||||
@@ -918,9 +999,15 @@ process.on('unhandledRejection', (err: any) => {
|
|||||||
// ─── Start ─────────────────────────────────────────────────────
|
// ─── Start ─────────────────────────────────────────────────────
|
||||||
async function start() {
|
async function start() {
|
||||||
// Clear old log files
|
// Clear old log files
|
||||||
try { fs.unlinkSync(CONSOLE_LOG_PATH); } catch {}
|
try { fs.unlinkSync(CONSOLE_LOG_PATH); } catch (err: any) {
|
||||||
try { fs.unlinkSync(NETWORK_LOG_PATH); } catch {}
|
if (err.code !== 'ENOENT') console.debug('[browse] Log cleanup console:', err.message);
|
||||||
try { fs.unlinkSync(DIALOG_LOG_PATH); } catch {}
|
}
|
||||||
|
try { fs.unlinkSync(NETWORK_LOG_PATH); } catch (err: any) {
|
||||||
|
if (err.code !== 'ENOENT') console.debug('[browse] Log cleanup network:', err.message);
|
||||||
|
}
|
||||||
|
try { fs.unlinkSync(DIALOG_LOG_PATH); } catch (err: any) {
|
||||||
|
if (err.code !== 'ENOENT') console.debug('[browse] Log cleanup dialog:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
const port = await findPort();
|
const port = await findPort();
|
||||||
|
|
||||||
@@ -955,18 +1042,24 @@ async function start() {
|
|||||||
// Check project-local designs first, then global
|
// Check project-local designs first, then global
|
||||||
const slug = process.env.GSTACK_SLUG || 'unknown';
|
const slug = process.env.GSTACK_SLUG || 'unknown';
|
||||||
const projectWelcome = `${process.env.HOME}/.gstack/projects/${slug}/designs/welcome-page-20260331/finalized.html`;
|
const projectWelcome = `${process.env.HOME}/.gstack/projects/${slug}/designs/welcome-page-20260331/finalized.html`;
|
||||||
try { if (require('fs').existsSync(projectWelcome)) return projectWelcome; } catch {}
|
try { if (require('fs').existsSync(projectWelcome)) return projectWelcome; } catch (err: any) {
|
||||||
|
console.warn('[browse] Error checking project welcome page:', err.message);
|
||||||
|
}
|
||||||
// Fallback: built-in welcome page from gstack install
|
// Fallback: built-in welcome page from gstack install
|
||||||
const skillRoot = process.env.GSTACK_SKILL_ROOT || `${process.env.HOME}/.claude/skills/gstack`;
|
const skillRoot = process.env.GSTACK_SKILL_ROOT || `${process.env.HOME}/.claude/skills/gstack`;
|
||||||
const builtinWelcome = `${skillRoot}/browse/src/welcome.html`;
|
const builtinWelcome = `${skillRoot}/browse/src/welcome.html`;
|
||||||
try { if (require('fs').existsSync(builtinWelcome)) return builtinWelcome; } catch {}
|
try { if (require('fs').existsSync(builtinWelcome)) return builtinWelcome; } catch (err: any) {
|
||||||
|
console.warn('[browse] Error checking builtin welcome page:', err.message);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
})();
|
})();
|
||||||
if (welcomePath) {
|
if (welcomePath) {
|
||||||
try {
|
try {
|
||||||
const html = require('fs').readFileSync(welcomePath, 'utf-8');
|
const html = require('fs').readFileSync(welcomePath, 'utf-8');
|
||||||
return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
||||||
} catch {}
|
} catch (err: any) {
|
||||||
|
console.error('[browse] Failed to read welcome page:', welcomePath, err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// No welcome page found — redirect to about:blank
|
// No welcome page found — redirect to about:blank
|
||||||
return new Response('', { status: 302, headers: { 'Location': 'about:blank' } });
|
return new Response('', { status: 302, headers: { 'Location': 'about:blank' } });
|
||||||
@@ -1046,7 +1139,8 @@ async function start() {
|
|||||||
const unsubscribe = subscribe((entry) => {
|
const unsubscribe = subscribe((entry) => {
|
||||||
try {
|
try {
|
||||||
controller.enqueue(encoder.encode(`event: activity\ndata: ${JSON.stringify(entry)}\n\n`));
|
controller.enqueue(encoder.encode(`event: activity\ndata: ${JSON.stringify(entry)}\n\n`));
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
|
console.debug('[browse] Activity SSE stream error, unsubscribing:', err.message);
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1055,7 +1149,8 @@ async function start() {
|
|||||||
const heartbeat = setInterval(() => {
|
const heartbeat = setInterval(() => {
|
||||||
try {
|
try {
|
||||||
controller.enqueue(encoder.encode(`: heartbeat\n\n`));
|
controller.enqueue(encoder.encode(`: heartbeat\n\n`));
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
|
console.debug('[browse] Activity SSE heartbeat failed:', err.message);
|
||||||
clearInterval(heartbeat);
|
clearInterval(heartbeat);
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
}
|
}
|
||||||
@@ -1065,7 +1160,9 @@ async function start() {
|
|||||||
req.signal.addEventListener('abort', () => {
|
req.signal.addEventListener('abort', () => {
|
||||||
clearInterval(heartbeat);
|
clearInterval(heartbeat);
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
try { controller.close(); } catch {}
|
try { controller.close(); } catch {
|
||||||
|
// Expected: stream already closed
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1214,7 +1311,9 @@ async function start() {
|
|||||||
chatBuffer = [];
|
chatBuffer = [];
|
||||||
chatNextId = 0;
|
chatNextId = 0;
|
||||||
if (sidebarSession) {
|
if (sidebarSession) {
|
||||||
try { fs.writeFileSync(path.join(SESSIONS_DIR, sidebarSession.id, 'chat.jsonl'), ''); } catch {}
|
try { fs.writeFileSync(path.join(SESSIONS_DIR, sidebarSession.id, 'chat.jsonl'), ''); } catch (err: any) {
|
||||||
|
console.error('[browse] Failed to clear chat file:', err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||||
}
|
}
|
||||||
@@ -1455,7 +1554,8 @@ async function start() {
|
|||||||
controller.enqueue(encoder.encode(
|
controller.enqueue(encoder.encode(
|
||||||
`event: inspector\ndata: ${JSON.stringify(event)}\n\n`
|
`event: inspector\ndata: ${JSON.stringify(event)}\n\n`
|
||||||
));
|
));
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
|
console.debug('[browse] Inspector SSE stream error:', err.message);
|
||||||
inspectorSubscribers.delete(notify);
|
inspectorSubscribers.delete(notify);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1465,7 +1565,8 @@ async function start() {
|
|||||||
const heartbeat = setInterval(() => {
|
const heartbeat = setInterval(() => {
|
||||||
try {
|
try {
|
||||||
controller.enqueue(encoder.encode(`: heartbeat\n\n`));
|
controller.enqueue(encoder.encode(`: heartbeat\n\n`));
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
|
console.debug('[browse] Inspector SSE heartbeat failed:', err.message);
|
||||||
clearInterval(heartbeat);
|
clearInterval(heartbeat);
|
||||||
inspectorSubscribers.delete(notify);
|
inspectorSubscribers.delete(notify);
|
||||||
}
|
}
|
||||||
@@ -1475,7 +1576,9 @@ async function start() {
|
|||||||
req.signal.addEventListener('abort', () => {
|
req.signal.addEventListener('abort', () => {
|
||||||
clearInterval(heartbeat);
|
clearInterval(heartbeat);
|
||||||
inspectorSubscribers.delete(notify);
|
inspectorSubscribers.delete(notify);
|
||||||
try { controller.close(); } catch {}
|
try { controller.close(); } catch (err: any) {
|
||||||
|
// Expected: stream already closed
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1523,9 +1626,13 @@ async function start() {
|
|||||||
const currentUrl = browserManager.getCurrentUrl();
|
const currentUrl = browserManager.getCurrentUrl();
|
||||||
if (currentUrl === 'about:blank' || currentUrl === '') {
|
if (currentUrl === 'about:blank' || currentUrl === '') {
|
||||||
const page = browserManager.getPage();
|
const page = browserManager.getPage();
|
||||||
page.goto(`http://127.0.0.1:${port}/welcome`, { timeout: 3000 }).catch(() => {});
|
page.goto(`http://127.0.0.1:${port}/welcome`, { timeout: 3000 }).catch((err: any) => {
|
||||||
|
console.warn('[browse] Failed to navigate to welcome page:', err.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch (err: any) {
|
||||||
|
console.warn('[browse] Welcome page navigation setup failed:', err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up stale state files (older than 7 days)
|
// Clean up stale state files (older than 7 days)
|
||||||
@@ -1542,7 +1649,9 @@ async function start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch (err: any) {
|
||||||
|
console.warn('[browse] Failed to clean stale state files:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[browse] Server running on http://127.0.0.1:${port} (PID: ${process.pid})`);
|
console.log(`[browse] Server running on http://127.0.0.1:${port} (PID: ${process.pid})`);
|
||||||
console.log(`[browse] State file: ${config.stateFile}`);
|
console.log(`[browse] State file: ${config.stateFile}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user