Files
gstack/docs/designs/SIDEBAR_MESSAGE_FLOW.md
T
Garry Tan 38bc431bbd docs: sidebar message flow architecture doc + CLAUDE.md pointer
SIDEBAR_MESSAGE_FLOW.md documents the full init timeline, message flow
(user types → claude responds), auth token chain, arrow hint signal
chain, model routing, tab concurrency, and known failure modes.

CLAUDE.md now tells you to read it before touching sidebar files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 07:21:20 -07:00

8.8 KiB

Sidebar Message Flow

How the GStack Browser sidebar actually works. Read this before touching sidepanel.js, background.js, content.js, server.ts sidebar endpoints, or sidebar-agent.ts.

Components

┌─────────────────┐     ┌──────────────┐     ┌─────────────┐     ┌────────────────┐
│  sidepanel.js   │────▶│ background.js│────▶│  server.ts   │────▶│sidebar-agent.ts│
│  (Chrome panel) │     │ (svc worker) │     │  (Bun HTTP)  │     │  (Bun process) │
└─────────────────┘     └──────────────┘     └─────────────┘     └────────────────┘
        ▲                                           │                      │
        │           polls /sidebar-chat             │    polls queue file   │
        └───────────────────────────────────────────┘                      │
                                                    ◀──────────────────────┘
                                                    POST /sidebar-agent/event

Startup Timeline

T+0ms     CLI runs `$B connect`
            ├── Server starts on port 34567
            ├── Writes state to .gstack/browse.json (pid, port, token)
            ├── Launches headed Chromium with extension
            └── Clears sidebar-agent-queue.jsonl

T+500ms   sidebar-agent.ts spawned by CLI
            ├── Reads auth token from .gstack/browse.json
            ├── Creates queue file if missing
            ├── Sets lastLine = current line count
            └── Starts polling every 200ms

T+1-3s    Extension loads in Chromium
            ├── background.js: health poll every 1s (fast startup)
            │     └── GET /health → gets auth token
            ├── content.js: injects on welcome page
            │     └── Does NOT fire gstack-extension-ready (waits for sidebar)
            └── Side panel: may auto-open via chrome.sidePanel.open()

T+2-10s   Side panel connects
            ├── tryConnect() → asks background for port/token
            ├── Fallback: direct GET /health for token
            ├── updateConnection(url, token)
            │     ├── Starts chat polling (1s interval)
            │     ├── Starts tab polling (2s interval)
            │     ├── Connects SSE activity stream
            │     └── Sends { type: 'sidebarOpened' } to background
            └── background relays to content script → hides welcome arrow

T+10s+    Ready for messages

Message Flow: User Types → Claude Responds

1. User types "go to hn" in sidebar, hits Enter

2. sidepanel.js sendMessage()
   ├── Renders user bubble immediately (optimistic)
   ├── Renders thinking dots immediately
   ├── Switches to fast poll (300ms)
   └── chrome.runtime.sendMessage({ type: 'sidebar-command', message, tabId })

3. background.js
   ├── Gets active Chrome tab URL
   └── POST /sidebar-command { message, activeTabUrl }
       with Authorization: Bearer ${authToken}

4. server.ts /sidebar-command handler
   ├── validateAuth(req)
   ├── syncActiveTabByUrl(extensionUrl) — syncs Playwright tab to Chrome tab
   ├── pickSidebarModel(message) — 'sonnet' for actions, 'opus' for analysis
   ├── Adds user message to chat buffer
   ├── Builds system prompt + args
   └── Appends JSON to ~/.gstack/sidebar-agent-queue.jsonl

5. sidebar-agent.ts poll() (within 200ms)
   ├── Reads new line from queue file
   ├── Parses JSON entry
   ├── Checks processingTabs — skips if tab already has agent running
   └── askClaude(entry) — fire and forget

6. sidebar-agent.ts askClaude()
   ├── spawn('claude', ['-p', prompt, '--model', model, ...])
   ├── Streams stdout line-by-line (stream-json format)
   ├── For each event: POST /sidebar-agent/event { type, tool, text, tabId }
   └── On close: POST /sidebar-agent/event { type: 'agent_done' }

7. server.ts processAgentEvent()
   ├── Adds entry to chat buffer (in-memory + disk)
   ├── On agent_done: sets tab status to 'idle'
   └── On agent_done: processes next queued message for that tab

8. sidepanel.js pollChat() (every 300ms during fast poll)
   ├── GET /sidebar-chat?after=${chatLineCount}&tabId=${tabId}
   ├── Renders new entries (text, tool_use, agent_done)
   └── On agent idle: removes thinking dots, stops fast poll

Arrow Hint Hide Flow (4-step signal chain)

The welcome page shows a right-pointing arrow until the sidebar opens.

1. sidepanel.js updateConnection()
   └── chrome.runtime.sendMessage({ type: 'sidebarOpened' })

2. background.js
   └── chrome.tabs.sendMessage(activeTabId, { type: 'sidebarOpened' })

3. content.js onMessage handler
   └── document.dispatchEvent(new CustomEvent('gstack-extension-ready'))

4. welcome.html script
   └── addEventListener('gstack-extension-ready', () => arrow.classList.add('hidden'))

The arrow does NOT hide when the extension loads. Only when the sidebar connects.

Auth Token Flow

Server starts → AUTH_TOKEN = crypto.randomUUID()
    │
    ├── GET /health (no auth) → returns { token: AUTH_TOKEN }
    │
    ├── background.js checkHealth() → authToken = data.token
    │     └── Refreshes on EVERY health poll (fixes stale token on restart)
    │
    ├── sidepanel.js tryConnect() → serverToken from background or /health
    │     └── Used for chat polling: Authorization: Bearer ${serverToken}
    │
    └── sidebar-agent.ts refreshToken() → reads from .gstack/browse.json
          └── Used for event relay: Authorization: Bearer ${authToken}

If the server restarts, all three components get fresh tokens within 10s (background health poll interval).

Model Routing

pickSidebarModel(message) in server.ts classifies messages:

Pattern Model Why
"click @e24", "go to hn", "screenshot" sonnet Deterministic tool calls, no thinking needed
"what does this page say?", "summarize" opus Needs comprehension
"find bugs", "check for broken links" opus Analysis task
"navigate to X and fill the form" sonnet Action-oriented, no analysis words

Analysis words (what, why, how, summarize, describe, analyze, read X and Y) always override action verbs and force opus.

Known Failure Modes

Failure Symptom Root Cause Fix
Stale auth token "Unauthorized" in input Server restarted, background had old token background.js refreshes token on every health poll
Tab ID mismatch Message sent, no response visible Server assigned tabId 1, sidebar polling tabId 0 switchChatTab preserves optimistic UI during switch
Sidebar agent not running Messages queue forever Agent process failed to spawn or crashed Check `ps aux
Agent stale token Agent runs but no events appear in sidebar sidebar-agent has old token from .gstack/browse.json Agent re-reads token before each event POST
Queue file missing spawnClaude fails Race between server start and agent start Both sides create file if missing
Optimistic UI blown away User bubble + dots vanish switchChatTab replaced DOM with welcome screen Preserved DOM when lastOptimisticMsg is set

Per-Tab Concurrency

Each browser tab can run its own agent simultaneously:

  • Server: tabAgents: Map<number, TabAgentState> with per-tab queue (max 5)
  • sidebar-agent: processingTabs: Set<number> prevents duplicate spawns
  • Two messages on same tab: queued sequentially, processed in order
  • Two messages on different tabs: run concurrently

File Locations

Component File Runs in
Sidebar UI extension/sidepanel.js Chrome side panel
Service worker extension/background.js Chrome background
Content script extension/content.js Page context
Welcome page browse/src/welcome.html Page context
HTTP server browse/src/server.ts Bun (compiled binary)
Agent process browse/src/sidebar-agent.ts Bun (non-compiled, can spawn)
CLI entry browse/src/cli.ts Bun (compiled binary)
Queue file ~/.gstack/sidebar-agent-queue.jsonl Filesystem
State file .gstack/browse.json Filesystem
Chat log ~/.gstack/sessions/<id>/chat.jsonl Filesystem