Chrome extension
bun run build # writes extension/dist
# chrome://extensions → Developer mode → Load unpacked → extension/distbun run build is the only step afterwards: the service worker polls GET /build-id (the built content script's mtime) and calls chrome.runtime.reload() when it changes, then the open tab refreshes itself. No clicking reload in chrome://extensions.
Same server, same session, same findings — open the app and the extension together and they stay in sync. On a PR page the extension adds bucket badges to GitHub's own file rows, collapses SKIP files, and docks a sidebar (shadow root, so no style bleed; drag its left edge to resize, double-click to reset). ⌥f pins the selected line, ⌥a asks about it. Clicking a file jumps to it — by DOM row when it's rendered, otherwise via GitHub's own diff-<sha256 of path> anchor, which also works from the Conversation tab.
All network runs in the service worker: extension-origin fetch sidesteps GitHub's connect-src CSP and one socket serves every tab. MV3 workers die after ~30s idle, so the server heartbeats every 20s to keep the socket alive, and the content script reconnects when it gets reaped anyway.
Diagnosing it
Both halves of the extension log to POST /log, which prints to the server journal — a content script's console isn't reachable from a terminal:
journalctl --user -u navidiff -f # [content] and [sw] lines land here
curl -s localhost:5177/preflight # git / claude / database / github app