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
+2 -1
View File
@@ -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();
}
+13
View File
@@ -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);
+5
View File
@@ -48,6 +48,11 @@
<div class="refs-footer" id="refs-footer"></div>
</main>
<!-- Experimental chat banner (shown when chatEnabled) -->
<div id="experimental-banner" class="experimental-banner" style="display: none;">
&#x26A0; Standalone mode &mdash; this is a separate agent from your workspace
</div>
<!-- Command Bar -->
<div class="command-bar">
<input type="text" class="command-input" id="command-input" placeholder="Message Claude Code..." autocomplete="off" spellcheck="false">
+37
View File
@@ -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');
}
}
}