mirror of https://github.com/garrytan/gstack.git
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:
parent
dff217838b
commit
0ae5838eb4
|
|
@ -469,9 +469,9 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
|
||||||
### Server
|
### Server
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `connect [browser] [--port N]` | Connect to real Chrome/Comet browser via CDP |
|
| `connect` | Launch headed Chromium with Chrome extension |
|
||||||
| `disconnect` | Disconnect from real browser, return to headless mode |
|
| `disconnect` | Disconnect headed browser, return to headless mode |
|
||||||
| `focus [@ref]` | Bring connected browser window to foreground (macOS) |
|
| `focus [@ref]` | Bring headed browser window to foreground (macOS) |
|
||||||
| `handoff [message]` | Open visible Chrome at current page for user takeover |
|
| `handoff [message]` | Open visible Chrome at current page for user takeover |
|
||||||
| `restart` | Restart server |
|
| `restart` | Restart server |
|
||||||
| `resume` | Re-snapshot after user takeover, return control to AI |
|
| `resume` | Re-snapshot after user takeover, return control to AI |
|
||||||
|
|
|
||||||
|
|
@ -597,9 +597,9 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
|
||||||
### Server
|
### Server
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `connect [browser] [--port N]` | Connect to real Chrome/Comet browser via CDP |
|
| `connect` | Launch headed Chromium with Chrome extension |
|
||||||
| `disconnect` | Disconnect from real browser, return to headless mode |
|
| `disconnect` | Disconnect headed browser, return to headless mode |
|
||||||
| `focus [@ref]` | Bring connected browser window to foreground (macOS) |
|
| `focus [@ref]` | Bring headed browser window to foreground (macOS) |
|
||||||
| `handoff [message]` | Open visible Chrome at current page for user takeover |
|
| `handoff [message]` | Open visible Chrome at current page for user takeover |
|
||||||
| `restart` | Restart server |
|
| `restart` | Restart server |
|
||||||
| `resume` | Re-snapshot after user takeover, return control to AI |
|
| `resume` | Re-snapshot after user takeover, return control to AI |
|
||||||
|
|
|
||||||
|
|
@ -391,14 +391,19 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
|
||||||
// Delete stale state file
|
// Delete stale state file
|
||||||
try { fs.unlinkSync(config.stateFile); } catch {}
|
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 {
|
try {
|
||||||
// Start server in headed mode with extension auto-loaded
|
// Start server in headed mode with extension auto-loaded
|
||||||
// Use a well-known port so the Chrome extension auto-connects
|
// Use a well-known port so the Chrome extension auto-connects
|
||||||
const newState = await startServer({
|
const serverEnv: Record<string, string> = {
|
||||||
BROWSE_HEADED: '1',
|
BROWSE_HEADED: '1',
|
||||||
BROWSE_PORT: '34567',
|
BROWSE_PORT: '34567',
|
||||||
});
|
};
|
||||||
|
if (chatMode) serverEnv.BROWSE_SIDEBAR_CHAT = '1';
|
||||||
|
const newState = await startServer(serverEnv);
|
||||||
|
|
||||||
// Print connected status
|
// Print connected status
|
||||||
const resp = await fetch(`http://127.0.0.1:${newState.port}/command`, {
|
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();
|
const status = await resp.text();
|
||||||
console.log(`Connected to real Chrome\n${status}`);
|
console.log(`Connected to real Chrome\n${status}`);
|
||||||
|
|
||||||
// Auto-start sidebar agent (non-compiled bun process)
|
// Auto-start sidebar agent only when --chat is enabled
|
||||||
const agentScript = path.resolve(__dirname, 'sidebar-agent.ts');
|
if (chatMode) {
|
||||||
const agentLogFile = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent.log');
|
const agentScript = path.resolve(__dirname, 'sidebar-agent.ts');
|
||||||
try {
|
const agentLogFile = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent.log');
|
||||||
// Clear old agent queue
|
try {
|
||||||
const agentQueue = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent-queue.jsonl');
|
// Clear old agent queue
|
||||||
try { fs.writeFileSync(agentQueue, ''); } catch {}
|
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], {
|
const agentProc = Bun.spawn(['bun', 'run', agentScript], {
|
||||||
cwd: config.projectDir,
|
cwd: config.projectDir,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
BROWSE_BIN: path.resolve(__dirname, '..', 'dist', 'browse'),
|
BROWSE_BIN: path.resolve(__dirname, '..', 'dist', 'browse'),
|
||||||
BROWSE_STATE_FILE: config.stateFile,
|
BROWSE_STATE_FILE: config.stateFile,
|
||||||
BROWSE_SERVER_PORT: String(newState.port),
|
BROWSE_SERVER_PORT: String(newState.port),
|
||||||
},
|
},
|
||||||
stdio: ['ignore', 'ignore', 'ignore'],
|
stdio: ['ignore', 'ignore', 'ignore'],
|
||||||
});
|
});
|
||||||
agentProc.unref();
|
agentProc.unref();
|
||||||
console.log(`[browse] Sidebar agent started (PID: ${agentProc.pid})`);
|
console.log(`[browse] Sidebar agent started (PID: ${agentProc.pid})`);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`[browse] Sidebar agent failed to start: ${err.message}`);
|
console.error(`[browse] Sidebar agent failed to start: ${err.message}`);
|
||||||
console.error(`[browse] Run manually: bun run ${agentScript}`);
|
console.error(`[browse] Run manually: bun run ${agentScript}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`[browse] Connect failed: ${err.message}`);
|
console.error(`[browse] Connect failed: ${err.message}`);
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ ensureStateDir(config);
|
||||||
const AUTH_TOKEN = crypto.randomUUID();
|
const AUTH_TOKEN = crypto.randomUUID();
|
||||||
const BROWSE_PORT = parseInt(process.env.BROWSE_PORT || '0', 10);
|
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 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 {
|
function validateAuth(req: Request): boolean {
|
||||||
const header = req.headers.get('authorization');
|
const header = req.headers.get('authorization');
|
||||||
|
|
@ -775,6 +776,7 @@ async function start() {
|
||||||
tabs: browserManager.getTabCount(),
|
tabs: browserManager.getTabCount(),
|
||||||
currentUrl: browserManager.getCurrentUrl(),
|
currentUrl: browserManager.getCurrentUrl(),
|
||||||
token: AUTH_TOKEN, // Extension uses this for Bearer auth
|
token: AUTH_TOKEN, // Extension uses this for Bearer auth
|
||||||
|
chatEnabled,
|
||||||
agent: {
|
agent: {
|
||||||
status: agentStatus,
|
status: agentStatus,
|
||||||
runningFor: agentStartTime ? Date.now() - agentStartTime : null,
|
runningFor: agentStartTime ? Date.now() - agentStartTime : null,
|
||||||
|
|
@ -873,6 +875,14 @@ async function start() {
|
||||||
|
|
||||||
// ─── Sidebar endpoints (auth required — token from /health) ────
|
// ─── 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
|
// Sidebar chat history — read from in-memory buffer
|
||||||
if (url.pathname === '/sidebar-chat') {
|
if (url.pathname === '/sidebar-chat') {
|
||||||
if (!validateAuth(req)) {
|
if (!validateAuth(req)) {
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ async function checkHealth() {
|
||||||
if (data.status === 'healthy') {
|
if (data.status === 'healthy') {
|
||||||
// Capture auth token from health response
|
// Capture auth token from health response
|
||||||
if (data.token) authToken = data.token;
|
if (data.token) authToken = data.token;
|
||||||
setConnected(data);
|
// Forward chatEnabled so sidepanel can show/hide chat tab
|
||||||
|
setConnected({ ...data, chatEnabled: !!data.chatEnabled });
|
||||||
} else {
|
} else {
|
||||||
setDisconnected();
|
setDisconnected();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -684,6 +684,19 @@ footer {
|
||||||
}
|
}
|
||||||
.port-input:focus { border-color: var(--amber-500); }
|
.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 ───────────────────────────────────── */
|
/* ─── Accessibility ───────────────────────────────────── */
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
outline: 2px solid var(--amber-500);
|
outline: 2px solid var(--amber-500);
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,11 @@
|
||||||
<div class="refs-footer" id="refs-footer"></div>
|
<div class="refs-footer" id="refs-footer"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Experimental chat banner (shown when chatEnabled) -->
|
||||||
|
<div id="experimental-banner" class="experimental-banner" style="display: none;">
|
||||||
|
⚠ Standalone mode — this is a separate agent from your workspace
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Command Bar -->
|
<!-- Command Bar -->
|
||||||
<div class="command-bar">
|
<div class="command-bar">
|
||||||
<input type="text" class="command-input" id="command-input" placeholder="Message Claude Code..." autocomplete="off" spellcheck="false">
|
<input type="text" class="command-input" id="command-input" placeholder="Message Claude Code..." autocomplete="off" spellcheck="false">
|
||||||
|
|
|
||||||
|
|
@ -612,6 +612,7 @@ chrome.runtime.onMessage.addListener((msg) => {
|
||||||
if (msg.data) {
|
if (msg.data) {
|
||||||
const url = `http://127.0.0.1:${msg.data.port || 34567}`;
|
const url = `http://127.0.0.1:${msg.data.port || 34567}`;
|
||||||
updateConnection(url, msg.data.token);
|
updateConnection(url, msg.data.token);
|
||||||
|
applyChatEnabled(!!msg.data.chatEnabled);
|
||||||
} else {
|
} else {
|
||||||
updateConnection(null);
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue