From 06002a82516362ed03cad9d5f4a5ab4584634ef3 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 20 Apr 2026 04:39:18 +0800 Subject: [PATCH] feat(security): shield icon continuous polling via /sidebar-chat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the v1 limitation noted in the shield icon follow-up TODO. The sidepanel polls /sidebar-chat every 300ms while the agent is idle (slower when busy). Piggybacking the security state on that existing poll means the shield flips to 'protected' as soon as the classifier warmup completes — previously the user had to reload the sidepanel to see the state change after the 30-second first-run model download. Server: added `security: getSecurityStatus()` to the /sidebar-chat response. The call is cheap — getSecurityStatus reads a small JSON file (~/.gstack/security/session-state.json) that sidebar-agent writes once on warmup completion. No extra disk I/O per poll beyond a single stat+read of a ~200-byte file. Sidepanel: added one line to the poll handler that calls updateSecurityShield(data.security) when present. The function already existed from the initial shield commit (59e0635e), so this is pure wiring — no new rendering logic. Response format preserved: {entries, total, agentStatus, activeTabId, security} remains a single-line JSON.stringify argument so the brittle sidebar-ux.test.ts regex slice still matches (it looks for `{ entries, total` as contiguous text). Closes TODOS.md item "Shield icon continuous polling (P2)". Co-Authored-By: Claude Opus 4.7 (1M context) --- browse/src/server.ts | 6 +++++- extension/sidepanel.js | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/browse/src/server.ts b/browse/src/server.ts index 30cbe642..2124cc4d 100644 --- a/browse/src/server.ts +++ b/browse/src/server.ts @@ -1873,7 +1873,11 @@ async function start() { const activeTab = browserManager?.getActiveTabId?.() ?? 0; // Return per-tab agent status so the sidebar shows the right state per tab const tabAgentStatus = tabId !== null ? getTabAgentStatus(tabId) : agentStatus; - return new Response(JSON.stringify({ entries, total: chatNextId, agentStatus: tabAgentStatus, activeTabId: activeTab }), { + // Piggyback security state on the existing 300ms poll. Cheap: + // getSecurityStatus reads ~/.gstack/security/session-state.json. + // Sidepanel uses this to flip the shield icon when classifier + // warmup completes after initial connect. + return new Response(JSON.stringify({ entries, total: chatNextId, agentStatus: tabAgentStatus, activeTabId: activeTab, security: getSecurityStatus() }), { status: 200, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'http://127.0.0.1' }, }); diff --git a/extension/sidepanel.js b/extension/sidepanel.js index 28821bc6..cb11d244 100644 --- a/extension/sidepanel.js +++ b/extension/sidepanel.js @@ -557,6 +557,12 @@ async function pollChat() { if (data.total === 0 && welcome) welcome.style.display = ''; } + // Shield icon state rides the chat poll (every 300ms in fast mode, + // slower when idle). When the ML classifier finishes warming after + // initial connect — typically 30s on first run — the shield flips + // from 'off' to 'protected' without the user needing to reload. + if (data.security) updateSecurityShield(data.security); + if (data.entries && data.entries.length > 0) { // Hide welcome on first real entry const welcome = document.getElementById('chat-welcome');