Communication flow
Side Panel (app.js)
│ chrome.runtime.connect("agent") — one port per request
▼
Service Worker (background.js)
│ fetch() to provider API
▼
AI Provider (Anthropic / OpenAI / Gemini)
Two parallel port connections fire on page open via Promise.all:
streamViaPort("analyze")— full analysis with agent loopquickHighlightPage()— fast highlight scan
Port message types
| Message | Meaning |
|---|---|
{ type: "status", message } |
Progress label shown in UI |
{ type: "tool_call", name, label } |
Tool being invoked |
{ type: "text_delta", text } |
Streamed text chunk |
{ type: "highlight_item", highlight, index } |
One highlight, emitted in real-time |
{ type: "highlights_result", summary, categorySummaries } |
Final highlights metadata |
{ type: "done" } |
Stream complete |
{ type: "error", message } |
Error — stops streaming |
Agent loop (Claude only)
runAgentLoop / runHighlightsAgentLoop — up to 12 iterations (4 for highlights).
Stop reasons:
end_turn→ breakpause_turn→ continue (server-side web_search still running)tool_use→ runfetch_url, re-submit with results
Tools: web_search_20250305 (server-side, Anthropic-hosted), fetch_url (client-side, fetchUrl() in background.js).
Streaming parser
parseAgentStream(response, port, onTextDelta?) reads the Anthropic SSE stream.
Optional onTextDelta callback intercepts text chunks — used by highlights to extract JSON objects in real-time via extractCompleteHighlights().
Real-time highlight extraction
extractCompleteHighlights(buffer, fromPos) — brace-depth character scanner that finds complete {"phrase":...} JSON objects in a mid-stream buffer without waiting for the full response. Returns { highlights, nextPos }.
Provider routing
getProviderConfig() reads provider, ${p}Key, ${p}Model from chrome.storage.local.
"anthropic"→ full agent loop with tools"openai"→runOpenAIStream— SSE streaming, no tools"gemini"→runGeminiStream+geminiProxyFetch— non-streaming, proxied through content script
Gemini CORS proxy
Service workers send Origin: chrome-extension://[id] which Google's API rejects.
Fix: geminiProxyFetch() sends the request to the content script via chrome.tabs.sendMessage. The content script fetches from the page's http/https origin, which Google accepts.
Use Content-Type: text/plain + key as URL query param to avoid CORS preflight (simple request).
Settings storage keys
| Key | Description |
|---|---|
provider |
"anthropic" / "openai" / "gemini" |
anthropicKey / openaiKey / geminiKey |
API keys |
anthropicModel / openaiModel / geminiModel |
Selected model |
apiKey |
Legacy Anthropic key (still read for backwards compat) |
Adding a new provider — checklist
- Add
const XXX_API_BASEconstant inbackground.js - Add to
getProviderConfig()key/model maps - Implement
runXxxStream(port, apiKey, model, systemText, messages, signal) - Branch in
handleAnalyze,handleChat,handleHighlights - Add provider tab to
options/index.htmland logic tooptions/app.js - Add API domain to
host_permissionsinmanifest.json - Update
CLAUDE.mdandREADME.md
Adding a new highlight category — checklist
- Add to
LABELSobject and add.ra-hl-{category}CSS rule incontent.js - Add to
CAT_METAinsidepanel/app.js - Add
.scan-pill.{cat},.hl-dot.{cat},.hl-item-dot.{cat},.summary-bullet-{cat}insidepanel/style.css - Add scan pill and legend entry in
sidepanel/index.html - Add category description in
buildHighlightsPrompt()inbackground.js - Update
CLAUDE.mdandREADME.md
Known limitations
- Gemini: no streaming (
generateContentonly), no tool use, no real URLs insource_url - OpenAI: no tool use, single-turn only
- Highlights agent loop: Claude only — non-Claude providers use
handleHighlightsSimple - Page content: truncated to 8000–10000 chars before sending to AI