feat: gate sidebar chat behind --chat flag

$B connect (default): headed Chromium + extension with Activity + Refs
tabs only. No separate agent spawned. Clean, no confusion.

$B connect --chat: same + Chat tab with standalone claude -p agent.
Shows experimental banner: "Standalone mode — this is a separate
agent from your workspace."

Implementation:
- cli.ts: parse --chat, set BROWSE_SIDEBAR_CHAT env, conditionally
  spawn sidebar-agent
- server.ts: gate /sidebar-* routes behind chatEnabled, return 403
  when disabled, include chatEnabled in /health response
- sidepanel.js: applyChatEnabled() hides/shows Chat tab + banner
- background.js: forward chatEnabled from health response
- sidepanel.html/css: experimental banner with amber styling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-22 21:25:49 -07:00
parent dff217838b
commit 0ae5838eb4
8 changed files with 105 additions and 32 deletions
+32 -25
View File
@@ -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<string, string> = {
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}`);
+10
View File
@@ -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)) {