feat(security): shield icon continuous polling via /sidebar-chat

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) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-20 04:39:18 +08:00
parent 758b3b373c
commit 06002a8251
2 changed files with 11 additions and 1 deletions
+5 -1
View File
@@ -1873,7 +1873,11 @@ async function start() {
const activeTab = browserManager?.getActiveTabId?.() ?? 0; const activeTab = browserManager?.getActiveTabId?.() ?? 0;
// Return per-tab agent status so the sidebar shows the right state per tab // Return per-tab agent status so the sidebar shows the right state per tab
const tabAgentStatus = tabId !== null ? getTabAgentStatus(tabId) : agentStatus; 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, status: 200,
headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'http://127.0.0.1' }, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'http://127.0.0.1' },
}); });
+6
View File
@@ -557,6 +557,12 @@ async function pollChat() {
if (data.total === 0 && welcome) welcome.style.display = ''; 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) { if (data.entries && data.entries.length > 0) {
// Hide welcome on first real entry // Hide welcome on first real entry
const welcome = document.getElementById('chat-welcome'); const welcome = document.getElementById('chat-welcome');