diff --git a/.agents/skills/gstack-browse/SKILL.md b/.agents/skills/gstack-browse/SKILL.md index 8649aef9..19b2db98 100644 --- a/.agents/skills/gstack-browse/SKILL.md +++ b/.agents/skills/gstack-browse/SKILL.md @@ -469,9 +469,9 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`. ### Server | Command | Description | |---------|-------------| -| `connect [browser] [--port N]` | Connect to real Chrome/Comet browser via CDP | -| `disconnect` | Disconnect from real browser, return to headless mode | -| `focus [@ref]` | Bring connected browser window to foreground (macOS) | +| `connect` | Launch headed Chromium with Chrome extension | +| `disconnect` | Disconnect headed browser, return to headless mode | +| `focus [@ref]` | Bring headed browser window to foreground (macOS) | | `handoff [message]` | Open visible Chrome at current page for user takeover | | `restart` | Restart server | | `resume` | Re-snapshot after user takeover, return control to AI | diff --git a/.agents/skills/gstack/SKILL.md b/.agents/skills/gstack/SKILL.md index 414fa2f3..93429955 100644 --- a/.agents/skills/gstack/SKILL.md +++ b/.agents/skills/gstack/SKILL.md @@ -597,9 +597,9 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`. ### Server | Command | Description | |---------|-------------| -| `connect [browser] [--port N]` | Connect to real Chrome/Comet browser via CDP | -| `disconnect` | Disconnect from real browser, return to headless mode | -| `focus [@ref]` | Bring connected browser window to foreground (macOS) | +| `connect` | Launch headed Chromium with Chrome extension | +| `disconnect` | Disconnect headed browser, return to headless mode | +| `focus [@ref]` | Bring headed browser window to foreground (macOS) | | `handoff [message]` | Open visible Chrome at current page for user takeover | | `restart` | Restart server | | `resume` | Re-snapshot after user takeover, return control to AI | diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 3c93c155..eee58f64 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -391,14 +391,19 @@ Refs: After 'snapshot', use @e1, @e2... as selectors: // Delete stale state file try { fs.unlinkSync(config.stateFile); } catch {} - console.log('Launching headed Chromium with extension...'); + const chatMode = commandArgs.includes('--chat'); + console.log(chatMode + ? 'Launching headed Chromium with extension + standalone chat (experimental)...' + : 'Launching headed Chromium with extension...'); try { // Start server in headed mode with extension auto-loaded // Use a well-known port so the Chrome extension auto-connects - const newState = await startServer({ + const serverEnv: Record = { BROWSE_HEADED: '1', BROWSE_PORT: '34567', - }); + }; + if (chatMode) serverEnv.BROWSE_SIDEBAR_CHAT = '1'; + const newState = await startServer(serverEnv); // Print connected status const resp = await fetch(`http://127.0.0.1:${newState.port}/command`, { @@ -413,29 +418,31 @@ Refs: After 'snapshot', use @e1, @e2... as selectors: const status = await resp.text(); console.log(`Connected to real Chrome\n${status}`); - // Auto-start sidebar agent (non-compiled bun process) - const agentScript = path.resolve(__dirname, 'sidebar-agent.ts'); - const agentLogFile = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent.log'); - try { - // Clear old agent queue - const agentQueue = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent-queue.jsonl'); - try { fs.writeFileSync(agentQueue, ''); } catch {} + // Auto-start sidebar agent only when --chat is enabled + if (chatMode) { + const agentScript = path.resolve(__dirname, 'sidebar-agent.ts'); + const agentLogFile = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent.log'); + try { + // Clear old agent queue + const agentQueue = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent-queue.jsonl'); + try { fs.writeFileSync(agentQueue, ''); } catch {} - const agentProc = Bun.spawn(['bun', 'run', agentScript], { - cwd: config.projectDir, - env: { - ...process.env, - BROWSE_BIN: path.resolve(__dirname, '..', 'dist', 'browse'), - BROWSE_STATE_FILE: config.stateFile, - BROWSE_SERVER_PORT: String(newState.port), - }, - stdio: ['ignore', 'ignore', 'ignore'], - }); - agentProc.unref(); - console.log(`[browse] Sidebar agent started (PID: ${agentProc.pid})`); - } catch (err: any) { - console.error(`[browse] Sidebar agent failed to start: ${err.message}`); - console.error(`[browse] Run manually: bun run ${agentScript}`); + const agentProc = Bun.spawn(['bun', 'run', agentScript], { + cwd: config.projectDir, + env: { + ...process.env, + BROWSE_BIN: path.resolve(__dirname, '..', 'dist', 'browse'), + BROWSE_STATE_FILE: config.stateFile, + BROWSE_SERVER_PORT: String(newState.port), + }, + stdio: ['ignore', 'ignore', 'ignore'], + }); + agentProc.unref(); + console.log(`[browse] Sidebar agent started (PID: ${agentProc.pid})`); + } catch (err: any) { + console.error(`[browse] Sidebar agent failed to start: ${err.message}`); + console.error(`[browse] Run manually: bun run ${agentScript}`); + } } } catch (err: any) { console.error(`[browse] Connect failed: ${err.message}`); diff --git a/browse/src/server.ts b/browse/src/server.ts index 959c249c..c82734fc 100644 --- a/browse/src/server.ts +++ b/browse/src/server.ts @@ -36,6 +36,7 @@ ensureStateDir(config); const AUTH_TOKEN = crypto.randomUUID(); const BROWSE_PORT = parseInt(process.env.BROWSE_PORT || '0', 10); const IDLE_TIMEOUT_MS = parseInt(process.env.BROWSE_IDLE_TIMEOUT || '1800000', 10); // 30 min +const chatEnabled = process.env.BROWSE_SIDEBAR_CHAT === '1'; function validateAuth(req: Request): boolean { const header = req.headers.get('authorization'); @@ -775,6 +776,7 @@ async function start() { tabs: browserManager.getTabCount(), currentUrl: browserManager.getCurrentUrl(), token: AUTH_TOKEN, // Extension uses this for Bearer auth + chatEnabled, agent: { status: agentStatus, runningFor: agentStartTime ? Date.now() - agentStartTime : null, @@ -873,6 +875,14 @@ async function start() { // ─── Sidebar endpoints (auth required — token from /health) ──── + // Gate all sidebar/chat routes behind --chat flag + if (!chatEnabled && url.pathname.startsWith('/sidebar')) { + return new Response(JSON.stringify({ error: 'Chat not enabled. Use: $B connect --chat' }), { + status: 403, + headers: { 'Content-Type': 'application/json' }, + }); + } + // Sidebar chat history — read from in-memory buffer if (url.pathname === '/sidebar-chat') { if (!validateAuth(req)) { diff --git a/extension/background.js b/extension/background.js index 21071044..ee4fa517 100644 --- a/extension/background.js +++ b/extension/background.js @@ -46,7 +46,8 @@ async function checkHealth() { if (data.status === 'healthy') { // Capture auth token from health response if (data.token) authToken = data.token; - setConnected(data); + // Forward chatEnabled so sidepanel can show/hide chat tab + setConnected({ ...data, chatEnabled: !!data.chatEnabled }); } else { setDisconnected(); } diff --git a/extension/sidepanel.css b/extension/sidepanel.css index 2c69c73c..85558961 100644 --- a/extension/sidepanel.css +++ b/extension/sidepanel.css @@ -684,6 +684,19 @@ footer { } .port-input:focus { border-color: var(--amber-500); } +/* ─── Experimental Banner ─────────────────────────────── */ +.experimental-banner { + background: rgba(245, 158, 11, 0.15); + border: 1px solid rgba(245, 158, 11, 0.3); + color: #F59E0B; + padding: 8px 12px; + border-radius: 6px; + font-size: 12px; + margin: 8px 12px; + text-align: center; + flex-shrink: 0; +} + /* ─── Accessibility ───────────────────────────────────── */ :focus-visible { outline: 2px solid var(--amber-500); diff --git a/extension/sidepanel.html b/extension/sidepanel.html index 5267ae24..abbffb99 100644 --- a/extension/sidepanel.html +++ b/extension/sidepanel.html @@ -48,6 +48,11 @@ + + +
diff --git a/extension/sidepanel.js b/extension/sidepanel.js index 5011e707..9ba7c5a2 100644 --- a/extension/sidepanel.js +++ b/extension/sidepanel.js @@ -612,6 +612,7 @@ chrome.runtime.onMessage.addListener((msg) => { if (msg.data) { const url = `http://127.0.0.1:${msg.data.port || 34567}`; updateConnection(url, msg.data.token); + applyChatEnabled(!!msg.data.chatEnabled); } else { updateConnection(null); } @@ -622,3 +623,39 @@ chrome.runtime.onMessage.addListener((msg) => { } } }); + +// ─── Chat Gate ────────────────────────────────────────────────── +// Show/hide Chat tab + command bar based on chatEnabled from server + +function applyChatEnabled(enabled) { + const commandBar = document.querySelector('.command-bar'); + const chatTab = document.getElementById('tab-chat'); + const banner = document.getElementById('experimental-banner'); + const clearBtn = document.getElementById('clear-chat'); + + if (enabled) { + // Chat is enabled: show command bar, chat tab, experimental banner + if (commandBar) commandBar.style.display = ''; + if (chatTab) chatTab.style.display = ''; + if (banner) banner.style.display = ''; + if (clearBtn) clearBtn.style.display = ''; + } else { + // Chat disabled: hide command bar, chat content, clear button + if (commandBar) commandBar.style.display = 'none'; + if (banner) banner.style.display = 'none'; + if (clearBtn) clearBtn.style.display = 'none'; + // If currently on chat tab, switch to activity + if (chatTab && chatTab.classList.contains('active')) { + chatTab.classList.remove('active'); + // Open debug tabs and show activity + const debugToggle = document.getElementById('debug-toggle'); + const debugTabs = document.getElementById('debug-tabs'); + if (debugToggle) debugToggle.classList.add('active'); + if (debugTabs) debugTabs.style.display = 'flex'; + const activityTab = document.getElementById('tab-activity'); + if (activityTab) activityTab.classList.add('active'); + const activityBtn = document.querySelector('.tab[data-tab="activity"]'); + if (activityBtn) activityBtn.classList.add('active'); + } + } +}