Sidebar-driven run navigation: replace the per-tab overlay with a native Firefox sidebar#4
Merged
Conversation
…, shared completion Slice 1 of the sidebar-nav migration (plan/sidebar-nav.md §4) — the pure coordinator groundwork, no UI yet. - types: add non-terminal `deferred` to WorkItemStatus (tab open but set aside; frees its batch slot, revisited at run end). - applyDefer(run, itemId): open → deferred only; never re-defers a pending item and never overrides a verdict (no-wedge). - selectBatch: count only `open` against the batch window (deferred freed its slot) but cap `open + deferred` at MAX_OPEN_TABS = 15 so a defer-everything run can't open the whole broker list; claim deferred brokers too (a deferred tab is still a live tab — no second variant). - applyStop now sweeps deferred items into run_stopped. - isComplete / progressOf: one shared definition of done (no pending/open/ deferred remain) and progress (deferred counts toward total, not done) for popup, options, and the sidebar to consume in later slices. +16 tests; suite 116 green, typecheck + build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e url helpers to shared Slice 2 of the sidebar-nav migration (plan/sidebar-nav.md §6/§7) — the pure data-model foundations. Additive/mechanical, no runtime behavior change. types.ts: - New inbound messages: SIDEBAR_GET_STATE, DEFER, NAVIGATE_BROKER_TAB (sidebar → background, all carry the pinned windowId) and CHALLENGE_DETECTED / CHALLENGE_RESOLVED (headless content → background). Added to ToBackground. - Optional windowId on VerdictMsg + CloseTabMsg (the sidebar isn't in a broker tab, so sender.tab can't identify the run). - Optional guidance on ItemInfoMsg. - Ping/Pong/Reinj kept — their consumers (content PING listener, background handlers, popup/options Restore-overlay) are gutted in later slices. - SidebarView/SidebarUpdateMsg intentionally deferred to Slice 3, designed with the state.ts machine that produces them. brokers.ts: optional search.guidance (generic, PII-free, results-state only); TruePeopleSearch left unset — only ever populated with an accurate note. url helpers: move isResultsPage + brokerHostname out of content/classify.ts into new src/shared/url.ts (background will classify page-type too); detectChallenge stays. Tests relocated to src/shared/url.test.ts. typecheck + 116 tests + build all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… vs transient views
Slice 3 of the sidebar-nav migration (plan/sidebar-nav.md §3/§5). Pure only — no DOM, no browser, no wiring (mirrors the coordinator.ts / classify.ts convention). The sidebar UI, background coordination, and content strip come in later slices. src/shared/types.ts: - SidebarView discriminated union (tag: `view`). Six resting views — no-run / guidance / verdict / challenge / revisit / done — plus the two transient states saving / recorded (kept for union completeness; the UI layer sets them imperatively around a verdict send, they are never derived). - ActiveItemInfo (the ItemInfoMsg render payload + derived pageType) for the active-item views; PageType; SidebarUpdateMsg. - Extracted RunProgress and reused it in ItemInfoMsg (identical shape → no behavior change) so the sidebar and the message share one definition. src/sidebar/state.ts — deriveView(run, focus, brokers?): pure derivation with precedence no-run > done > challenge > guidance > verdict > revisit. Uses isResultsPage (shared/url) for the results↔details split and progressOf / isComplete (coordinator) for counts/completion. `brokers` is injectable (mirrors buildItems). Reconciled two §3 gaps before coding: - Gap A: added the `done` view §5 referenced but §3 omitted (doc fixed in 4a79712). `no-run` (never started) and `done` (finished) are distinct. - Gap B: saving/recorded are transient, so deriveView returns only resting views; a test asserts it never emits them. Load-bearing (Slice-1 review finding #2): when a pending AKA is stranded behind a deferred sibling broker (selectBatch claims deferred brokers, so no open tab exists to act on) and nothing is focused, deriveView routes to `revisit`, never `no-run`. Covered by an explicit test. +17 tests; suite 133 green, typecheck + build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 4 of the sidebar-nav migration (plan/sidebar-nav.md §6). The sidebar owns all verdict UI (later slices), so the content script no longer paints anything — it's now ~45 lines (was 679) that only report bot-challenge transitions and never touch the page DOM. Removed (~630 lines): OVERLAY_STYLES, createOverlayShell, all panel builders (buildVerdictPanel / buildGuidancePanel / buildChallengePanel / showMainPanel), setOverlayState, verdictMsg, sendVerdict, closeSelfTab, the GET_ITEM init call, the PING listener, and the now-unused imports (Verdict, ItemInfoMsg, isResultsPage, brokerHostname). Kept / new: - Reuses the already-tested detectChallenge() from ./classify. - On load, if a challenge is up: send CHALLENGE_DETECTED, then a MutationObserver on documentElement watches for it clearing → CHALLENGE_RESOLVED + disconnect. The 250 ms dismiss debounce is lifted verbatim from the old buildChallengePanel (guards Turnstile briefly detaching its container mid-transition). - Bare messages; background identifies the tab via sender.tab.id (this runs in the broker tab), same as GET_ITEM did. - Idempotency latch window.__expurgeReporterBound (mirrors the old __expurgePingBound) so re-injection — manifest auto-inject on nav, plus background's still-present reinjectIfMissing until Slice 5 — can't stack observers or emit duplicate CHALLENGE_DETECTED. Parity with the old overlay: challenge-detect on load + observe-until-resolved. Did NOT add the optional mid-run "challenge appears after a clean load" detection (the old overlay only detected on init); can revisit if rate-limiting mid-run needs it. Scope notes: - Background PING/PONG/REINJECT_OVERLAY/GET_ITEM handlers + reinjectIfMissing left intact (removed in Slice 5). During the gap, background PINGs get no response and it re-executeScripts the reporter — harmless (the latch no-ops it), just wasteful. Expected. - The extension is intentionally non-functional end-to-end after this slice: no overlay, and the sidebar UI doesn't exist until Slice 6. Runs open tabs with no verdict UI until Slices 5-6 land. No new tests (thin imperative glue over the already-covered detectChallenge). dist/content.js: 52844 → 35775 bytes (~17KB of overlay code gone; remaining floor is the bundled webextension-polyfill). typecheck + 133 tests + build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 5a of the sidebar-nav migration (plan §5). Threads a windowId through the
run so batch tabs open in the run's window (the sidebar is a window-level surface).
- StartRunMsg gains optional windowId; RunState gains windowId (session-only, but
not a recycled-id hazard like tabId, so safe to persist in session storage).
- handleStartRun resolves windowId: msg.windowId → sender.tab.windowId →
windows.getLastFocused() (§7 will have popup/options pass it explicitly).
- openNextBatch creates tabs with { windowId: run.windowId }.
typecheck + 133 tests + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 5b of the sidebar-nav migration (plan §5). Background builds SidebarFocus
inputs and lets deriveView (sidebar/state.ts) own the view; it never re-derives.
- SIDEBAR_GET_STATE{windowId} (PULL): resolve the window's broker tab (prefer
active, else any tracked broker tab in the window via the window-scoped
findWindowBrokerTab — the old findActiveBrokerTab scan, retained), build focus,
return deriveView. A window without the run → no-run.
- Push SIDEBAR_UPDATE on tabs.onActivated, tabs.onUpdated complete, and challenge
messages. Sticky-view contract: pushActiveView reflects the ACTIVE broker tab
only and no-ops when the active tab isn't a broker tab, so a glance at another
tab won't flip the sidebar to revisit.
- SIDEBAR_UPDATE is window-scoped (windowId added): runtime.sendMessage broadcasts
to every sidebar, so each ignores mismatched windowIds.
- Per-tab challenge flag from CHALLENGE_DETECTED/RESOLVED, keyed by sender.tab.id
in storage.session (survives spindown mid-challenge; an in-memory Map wouldn't).
Cleared on RESOLVED, on tab close, AND on tabs.onUpdated complete to an on-host
page — Cloudflare interstitials resolve by redirect so RESOLVED never fires
(Slice-4 finding). New pure isOnHost (shared/url.ts, +5 tests) centralizes the
on-host/off-host check (also replaces the inline hostname math in the tab scan).
reinjectIfMissing stays wired in onUpdated for now (removed in 5d); it's a
harmless no-op ping against the headless content script.
typecheck + 138 tests + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 5c of the sidebar-nav migration (plan §5). Wires the run's action loop to the sidebar era. - nextFocusTarget(run) (coordinator, pure, +4 tests): the first `open` item, or null. Callers openNextBatch first, so a leftover `pending` is blocked behind a deferred/open sibling (finding #2) and is NOT force-opened — null routes deriveView to revisit. deferred is never an auto-focus target. - driveFocus/advance: after an action, openNextBatch (fill the freed slot) then activate the next target's tab and push its view; if none → push the focus=null view (revisit while deferred/blocked-pending remain, else done). ensureItemTab reopens a fresh tab from renderedUrl when a target has no live tab (resume / finding #3 tolerance). tabIdForItem reverse-resolves the expurge_tab_ map. - handleVerdict now comes from the sidebar (sender.tab is the sidebar, not the broker tab): resolve broker tab via tabIdForItem, capture listingUrl from the tab's own URL for a details-page verdict (captureListingUrl; explicit paste URL still wins), record, drop tracking, advance, then background closes the tab — the sidebar's 800 ms recorded animation is a UI transient and doesn't gate it. - handleSkip (tab_closed) now advances too. handleDefer + DEFER handler: applyDefer, keep the tab, advance. NAVIGATE_BROKER_TAB points the broker tab at a pasted URL. CLOSE_TAB kept vestigial (background owns the close now). - openNextBatch returns the updated run. typecheck + 142 tests + build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 5d of the sidebar-nav migration (plan §5). Deletes the overlay-era machinery now that the sidebar drives everything. Removed: - reinjectIfMissing + the PING/PONG roundtrip, findActiveBrokerTab (superseded by the window-scoped findWindowBrokerTab in 5b), the GET_ITEM/ITEM_INFO handler (content no longer pulls item info — deriveView→progressOf is the single view/ progress source, finding #1), the PING handler, and the reinject body of tabs.onUpdated (now purely pushes updates). browser.scripting is no longer used (the manifest's now-dead scripting permission is cleaned in Slice 6). - Dead types GetItemMsg / PingMsg / PongMsg and their ToBackground entries (grepped: zero senders). ReinjMsg KEPT — the popup/options "Restore overlay" buttons still send REINJECT_OVERLAY until §7; with the background handler gone they simply no-op ("Nothing to restore") until Slice 6 deletes the buttons. Resume / finding #3: no open→pending revert added. In the session-storage model, browser.storage.session (run + expurge_tab_ keys) AND the broker tabs all survive event-page spindown together, so open items keep valid tab links — a blanket revert on every loadRun would be wrong. ensureItemTab (5c) already reopens a fresh tab from renderedUrl when a target has no live tab, which is the tolerance the finding asks for; Slice 6's revisit-click will use it to reopen a resumed deferred item. Closing a deferred tab in-session still → skipped/tab_closed (unchanged). typecheck + 142 tests + build green. (Extension remains non-functional end-to-end until the sidebar UI lands in Slice 6.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice-5 review found a challenge-flag lifecycle bug on ON-HOST Cloudflare interstitials (broker.com/cdn-cgi/...): the content script set the flag at document_end (CHALLENGE_DETECTED), then tabs.onUpdated complete fired later and cleared it on the same load, so deriveView yielded `verdict` — "Is this you? Yes/No" over a "checking your browser" page. Root cause: split source of truth. The content script only reported DETECTED-on- load + RESOLVED-via-observer (nothing on a clean load), so a background onUpdated heuristic was bolted on to cover the redirect-to-clean case — but it couldn't tell "clean content" from "on-host challenge page," so it misfired. Fix — make the content script authoritative per-load, drop the background guess: - content/index.ts: report state on EVERY load — CHALLENGE_DETECTED if challenged (+ the existing MutationObserver for in-page Turnstile clears), else CHALLENGE_RESOLVED on a clean load. Idempotency guard kept. (This is Slice 4's "optional bidirectional detect" — it's load-bearing for on-host challenges. A challenge appearing after a clean load with no navigation stays out of scope.) - background/index.ts: remove the onUpdated challenge-flag clear and its now-dead item/tab/onHost computation; onUpdated is just "tracked broker tab + run → pushActiveView". Flag still cleared on CHALLENGE_RESOLVED and onRemoved. isOnHost stays imported (findWindowBrokerTab still uses it). Now the on-host interstitial reports DETECTED and stays challenged (nothing clears it); the redirect to the real page reports RESOLVED. No race, no misfire; the observer still covers in-page clears that don't navigate. plan/sidebar-nav.md §5/§6 updated to match (per-load content-authoritative). Also refreshed a stale content-script comment (Slice 5 removed the reinject path). No new tests (detectChallenge already covered; reporter/onUpdated are imperative glue, fully verifiable only once the sidebar lands in Slice 6). typecheck + 142 tests + build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves Decision B (clickable checklist rows) — the last backend piece before
the Slice 6 UI. The sidebar can't focus a tab itself (tab ids are background-only),
so it names an item and background jumps to it. One message serves both a checklist
row click and the revisit button (revisit = FOCUS_ITEM on the first deferred item).
- coordinator.promoteToOpen(run, itemId) (pure, +5 tests): deferred → open only,
the exact inverse of applyDefer. No-op on open/pending/verdicted. A pending item
must go through ensureItemTab (which creates its tab), so promoteToOpen never
conjures an `open` item with no live tab.
- FocusItemMsg { itemId, windowId } → ToBackground.
- handleFocusItem (serialWrite, handleDefer pattern): bail on no run / wrong window
/ already-verdicted item; ensureItemTab reopens a lost tab from renderedUrl
(resume / finding #3), flipping a tabless item to open; promoteToOpen flips a
still-alive deferred item to open (so a re-defer during revisit doesn't no-op);
saveRun; activate the tab; push deriveView(...) for the window.
- handleStartRun now pushActiveView after the first batch opens — init-race
insurance (Slice-5 review): a sidebar that raced SIDEBAR_GET_STATE ahead of the
run being saved corrects itself once tabs open.
typecheck + 147 tests + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng perm
Slice 6a of the sidebar-nav migration (plan §1/§2). Makes the sidebar loadable.
- manifest.json: add sidebar_action (default_panel dist/sidebar.html). Drop the
now-dead `scripting` permission (reinjectIfMissing/executeScript went away in
Slice 5 — grep confirms zero browser.scripting refs). Refresh the m9 note (the
overlay is gone; verdict UI is the window-level sidebar, no per-domain entry).
- build.mjs: esbuild src/sidebar/index.ts → dist/sidebar.js and style.css →
dist/sidebar.css; copyStatics copies index.html → dist/sidebar.html.
- src/sidebar/{index.html,style.css,index.ts}: minimal but real — listener-FIRST
init (attach onMessage, then windows.getCurrent → windowId, then
SIDEBAR_GET_STATE), windowId-filtered pushes, renders the view tag + shared
progressOf. The full checklist UI lands in 6b.
typecheck + 147 tests + build green; dist/sidebar.{html,js,css} emitted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 6b of the sidebar-nav migration (plan §3). The sidebar renders the eight views; it never re-derives (the background's deriveView is the source of truth). - Renders all 8 views. Buttons → messages: verdict cluster (hit/clear/unknown/ skip) and challenge "Skip this site" → VERDICT; guidance "Not found" → VERDICT clear + paste-URL → NAVIGATE_BROKER_TAB; Defer control (guidance/verdict/ challenge) → DEFER; revisit → FOCUS_ITEM on the first deferred item; done → open dashboard. - Transient saving/recorded (UI-owned, never derived): a verdict click owns the panel through saving → recorded (~800 ms) while a `transient` latch suppresses incoming pushes, then re-pulls the resting view (background already advanced). - Checklist (Decision A): grouped In progress / Waiting / Done from GET_RUN_STATE, re-fetched on each update; non-terminal rows clickable → FOCUS_ITEM (manual override, decision 5). Keyboard-accessible (role=button, Enter/Space). - Progress header via shared progressOf. DATA-INJECTION INVARIANT (STYLEGUIDE §0): renders only generic broker data — broker names, generic `exposes` chips, broker `guidance`, and a generic "alternate name" tag for AKA variants (from nameVariant, never the actual name). The user's real data — variantFirst/variantLast, renderedUrl/listingUrl (carry the searched name) — is deliberately never displayed. All dataset text via textContent, never innerHTML. Tokens only; no hard-coded values. typecheck + 147 tests + build green. (End-to-end behavior verifiable once loaded in Firefox — manual QA after 6c.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hared progress Slice 6c of the sidebar-nav migration (plan §7). - options handleStartRun: open the sidebar SYNCHRONOUSLY in the Start click's user gesture, before any await (sidebarAction.open() needs a gesture and opens in the active window — Q-015); then capture windowId (windows.getCurrent) and send it with START_RUN so the run is pinned to that window. Profile guard moved up so we don't open a sidebar / prompt for permission with no profile. ⚠ Q-015 empirical: the gesture now drives BOTH sidebarAction.open() and permissions.request(); flagged in a code comment to verify in Firefox 140+. - Removed the "Restore overlay" button + REINJECT_OVERLAY handler from BOTH popup and options; deleted the now-senderless ReinjMsg type + its ToBackground entry (grep confirms zero senders). - Replaced inline completion math in popup + options with shared isComplete / progressOf (one definition; deferred counts toward total, not done). typecheck + 147 tests + build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…idebar - sidebar-nav.md: mark §1/§2/§3/§5/§6/§7 ✅ DONE with commit refs; document Decision A under §3 (detail+view from the pushed SidebarView; grouped checklist from GET_RUN_STATE re-fetched per update; skew negligible; PII invariant on rows). - expurge-progress.md: flip the overlay→sidebar item from planned to done; note the sidebar gained an interactive clickable checklist beyond the old overlay, and that the pre-migration content/popup/background rows are superseded.
The sidebar migration removed webNavigation's last would-be user (it was only ever reserved, never wired — grep confirms zero refs in src/). An unused permission is an AMO-review smell, same as the scripting perm dropped in 6a. permissions → ["storage","tabs","downloads"]. The _notes breadcrumb is kept and rewritten to "re-add when M9 wires webNavigation.onErrorOccurred". dist/manifest.json valid; typecheck + 147 tests + build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…atestRun) renderView renders the view, THEN refreshChecklist() async-sets latestRun. But renderRevisit read latestRun to find the first deferred item — so when the sidebar's FIRST render was `revisit` (reopen mid-revisit / after resume), latestRun was still null and the "Revisit set-aside sites" button rendered DISABLED (the clickable Waiting rows still worked, but the button looked broken). Make the pure function the source of truth: deriveView's revisit branch computes focusId = first deferred ?? first pending ?? null (first pending covers the blocked-behind-deferred case; FOCUS_ITEM's ensureItemTab opens a pending one) and carries it in the view. renderRevisit takes focusId from the view and disables the button only when it's null — no dependency on the async checklist fetch. The now- unused module-level `latestRun` is dropped (made local in refreshChecklist). types: revisit variant gains `focusId: string | null`. state.test.ts: revisit tests assert focusId (first deferred; first pending when no deferred). plan §3 notes the carried focusId. typecheck + 148 tests + build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every surface (popup, options, sidebar) loaded fonts from fonts.googleapis.com, so for an extension whose pitch is "nothing about the user leaves their machine," opening any surface pinged Google (IP + usage). Pre-existing (the migration only matched the popup/options pattern); fixing it project-wide. Vendored the latin-subset woff2 locally (SIL OFL 1.1, redistribution fine — src/styles/fonts/LICENSE.md carries per-family copyrights + the full license): - Hanken Grotesk (variable wght axis, one file covers 400/500/600), 35 KB - IBM Plex Mono 400, 15 KB - Newsreader 600, 24 KB - src/styles/fonts.css: @font-face blocks with url("fonts/*.woff2"), font-display swap. Loaded by all three surfaces via a plain <link> — NOT bundled by esbuild, so the woff2 url()s resolve verbatim against dist/ (no woff2 loader needed). - build.mjs copyStatics: copy fonts.css → dist/fonts.css and src/styles/fonts → dist/fonts (woff2 + LICENSE). - Removed the 3 Google <link> tags (2 preconnect + 1 stylesheet) from all three index.html; added <link href="fonts.css">. Glyphs outside the latin subset fall back to the system stacks already in tokens.css. Verified: zero googleapis/gstatic refs in src or dist; woff2 present in dist/fonts; no external network request on surface open. typecheck + 148 tests + build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Firefox auto-opens a sidebar_action panel when the extension installs/loads, which is confusing next to the options page that already opens on install (onInstalled → openOptionsPage). Add the Firefox-specific sidebar_action.open_at_install: false (this is a Firefox-only extension) so on load only the options page opens; the sidebar opens only on the Start-run click (and the new Show-scan-panel button). dist/manifest.json valid; build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Once the sidebar is closed there was no way back. Add a "Show scan panel" button that calls browser.sidebarAction.open() (synchronous in the click gesture; opens in the active window, which then SIDEBAR_GET_STATEs — showing the run if it's the run's window, else no-run). No windowId plumbing needed. - popup (primary): button in the active + done states (a run exists); absent on no-run. Reuses the existing .popup-actions flex row — no new CSS. - options (secondary): same button in the Run section's active action-row. typecheck + 148 tests + build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Run-mutating actions from OUTSIDE the sidebar (Stop from popup/options, Delete-all
from options) didn't push a sidebar update, so the sidebar kept showing live
verdict/guidance controls for a run that had ended.
- handleStopRun: after applyStop + saveRun + clearing tab keys, push the resting
view to the run's window. A stopped run isComplete, so deriveView(updated, null)
yields `done` — sidebar drops the actionable controls. deriveView, not a
hardcoded view, keeps one source of truth.
- DELETE_ALL: capture the run's windowId BEFORE wiping session storage, then push
{ view: 'no-run' } after clearing — sidebar shows "No active scan in this window."
REVERDICT/MARK_SENT left as-is (they edit already-completed items — minor drift
only); follow-up if the done hit-count ever reads stale.
No test changes (imperative push, like the other handlers). typecheck + 148 tests
+ build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After a Stop, the run is isComplete so deriveView returned `done` — "Checked N of
N. You're all clear here." — which overclaims: the run_stopped items were
abandoned, not checked, and the user isn't "all clear."
Add a `stopped` SidebarView variant, derived when isComplete AND some item has
skipReason 'run_stopped' (checked before the `done` branch — a stopped run can't
also be a clean done). Payload { checked, total, hits } where `checked` excludes
the abandoned run_stopped items (they stay in `total`, which still excludes
missing: skips). Copy: "Scan stopped. Checked X of Y. The rest are still on your
list — start again anytime." — framed as what's left, no alarm (STYLEGUIDE voice).
handleStopRun / SIDEBAR_GET_STATE need no change — both route through deriveView,
so the stop push and any pull now yield `stopped` automatically.
+4 deriveView tests (checked excludes run_stopped; stays done without one;
precedence over a focused item; missing excluded from total). resting-views test
updated. typecheck + 152 tests + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Firefox 140+ QA (2026-07-01): a single Start click drove both sidebarAction.open() and permissions.request() — the host-permission prompt appeared, the sidebar opened, and the broker tab held until the grant. No reorder needed. Q-015 closes the last open thread from the sidebar-nav migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ailure)
castVerdict fired the VERDICT message with .catch(()=>{}) and rendered `recorded`
unconditionally — no ACK check, no timeout, no retry. That violated CLAUDE.md's
verdict contract ("wait for explicit ACK that background wrote to storage → then
show recorded; no ACK within timeout → retry"), which the old content-script
sendVerdict honored before the strip (6bf6270~1).
- sendVerdictAck: races each send against a 6s timeout, up to 3 attempts, returns
true iff reply.type === 'ACK'. Restores the old contract in the sidebar.
- castVerdict: saving → await ACK. On failure, re-pull the true state (the verdict
may or may not have landed) and append an inline "Couldn't save just now — try
again" while leaving the verdict controls usable — NEVER renders recorded on an
unconfirmed write. On success, recorded → 800 ms → re-pull. Both re-pulls are
now .catch()-guarded (finding #7) so a rejected SIDEBAR_GET_STATE can't dead-end
the panel.
Retry correctness depends on the handleVerdict idempotency guard (next commit): a
retry of a landed-but-ack-lost verdict must re-ACK without re-recording/-closing.
typecheck + 152 tests + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refreshChecklist fetched GET_RUN_STATE — the GLOBAL run, no windowId — so an idle
window's sidebar showed the run window's checklist + progress, even though its
detail panel correctly derived no-run. Its rows looked clickable but
FOCUS_ITEM{windowId:idle} was rejected by the background → dead clicks.
The detail view is already windowId-scoped: a no-run view means THIS window has no
run. So renderView now clears #checklist + #progress on no-run and only fetches
the checklist for other views (where the single pinned run IS this window's run,
so the global fetch is correct).
typecheck + 152 tests + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o-wedge)
withVerdict overwrites any item unconditionally (handleReverdict relies on that to
re-verdict already-verdicted items from the dashboard). So the no-wedge guard goes
in handleVerdict (the live-run path), not withVerdict: after loadRun, bail if the
item is missing or already `verdicted`.
Makes verdicts idempotent — the sidebar's retry (previous commit) of a landed-but-
ack-lost verdict no-ops and re-ACKs instead of re-recording/-advancing/-closing —
and stops a fast second verdict (Yes then No) from clobbering a recorded hit. The
message listener still returns {type:'ACK'} regardless, so the retry still confirms.
typecheck + 152 tests + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The old guidance overlay warned "doesn't look like a {host} URL" on an off-broker
paste; the sidebar dropped both the warning and any tab-targeting guarantee.
- Host warning: renderGuidance derives the broker host via brokerHostname(item.
renderedUrl) and, on input, shows a non-blocking warning (reusing the pure,
tested isOnHost) when the pasted host isn't the broker's (exact or subdomain).
Warn, never block. Only the hostname is shown — the full renderedUrl (which
carries the searched name) stays unrendered, honoring the PII invariant.
- Right tab: NavigateBrokerTabMsg gains itemId; the sidebar sends item.itemId and
the handler navigates tabIdForItem(itemId) instead of the active-preferred
findWindowBrokerTab — so if the active tab changed since the guidance view
rendered, the paste still lands in that item's own broker tab, not another's.
No new tests (isOnHost/brokerHostname already covered; the rest is UI/bg glue).
typecheck + 152 tests + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e visible QA reported an off-host paste showing no warning. The logic and the freshly-built bundle were already correct (off-host paste → warn.hidden=false), so this pins the behaviour down with a real test and makes the warning harder to miss. - Extract wirePasteWarning / shouldWarnOffHost (src/sidebar/paste.ts) — the pure warn-decision plus the input→warning DOM wiring, so the exact path a paste takes (set value → 'input' event → warn.hidden toggles) is unit-testable. - paste.test.ts (jsdom, +8): off-host/on-host/subdomain/empty/unparseable decisions, and the event path — an off-host paste shows the warning, an on-host one hides it, clearing hides it. Green, so the wiring is confirmed working. - renderGuidance uses wirePasteWarning (same behaviour, now tested). - Bump .paste-warning to fs-small + medium weight so it's not easy to overlook. If a manual test still shows no warning, it's a stale loaded add-on — reload it in about:debugging (the built dist contains the code, grep-confirmed). typecheck + 160 tests + build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
QA surfaced it: navigate the broker tab off-host (address bar → google.com) and
the sidebar still showed "Could this listing be you? Yes/No", letting you confirm
a listing on google.com — which would record a bogus hit (listingUrl=google.com)
and could generate a wrong opt-out draft. deriveView treated every non-results
page as "details" → verdict, including off-host ones. (This is the off-host
verdict-view finding from the deferred cluster; the demo made it worth doing now.)
- New `offsite` SidebarView. deriveView gates guidance/verdict on
isOnHost(tabUrl, renderedUrl), checked after challenge (a Cloudflare interstitial
is legitimately off-host) and before results/details — so an off-host page, OR a
lookalike host sitting at the results pathname (isResultsPage only compares the
path), can no longer read as verdict/guidance. null/unknown tabUrl → offsite too
(conservative: don't offer confirm unless we can confirm the host).
- renderOffsite: "This tab isn't on {broker} right now" + a "Back to my results"
action (NAVIGATE_BROKER_TAB to renderedUrl — used as a nav target, never shown,
PII-safe) + Defer.
- +4 deriveView tests (off-host → offsite, lookalike host → offsite, challenge
still wins off-host, on-host unaffected); null-tabUrl test updated verdict→offsite;
resting-set test updated. plan §3 documents it (eight resting views).
Note: this fixes the off-host *verdict* path. The paste-box warning (separate,
verified working) guards the sidebar's paste field — a different entry point than
the address bar.
typecheck + 164 tests + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix 1 (verdict ACK) hoisted `transient = false` to right after sendVerdictAck, so on the success path the latch was open during the recorded + 800 ms window. But handleVerdict pushes the NEXT item's SIDEBAR_UPDATE before returning the ACK (it calls advance first), so that push races the reply — landing during the unguarded window, it renders the next item and clobbers the "✓ recorded" confirmation. The latch exists to prevent exactly that yank; timing-dependent, so it passed casual testing and would flake in the wild. Move `transient = false` out of the hoisted spot into the two branches: on failure, clear it immediately (restore controls to retry); on success, clear it only AFTER the 800 ms recorded animation. The trailing pullState still re-derives the true latest state (next item, or a Stop/Delete that arrived during the suppressed window), so suppressing pushes for 800 ms loses nothing. typecheck + 164 tests + build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ts pathnameOf The new src/sidebar/index.ts is the sidebar's imperative render/message-wiring shell — same category as the background/content/options/popup entrypoints, which are all excluded from coverage (they run addListener/init at import and aren't unit-tested; their pure logic is extracted and covered). It was just missing from the exclude list, so its 0% dragged the global totals under threshold. Added it — its pure parts (state.ts, paste.ts) are already covered. Also covered state.ts's pathnameOf catch branch (unparseable tab URL → offsite) so the pure module is back to 100%, matching every other pure module. And refreshed the stale content/index.ts exclude comment (it's a headless challenge reporter now, not an overlay injector). Coverage: 100% statements/branches/functions/lines, thresholds pass. typecheck + 165 tests + build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the shadow-DOM overlay injected into every broker page with a Firefox native sidebar_action panel that drives the whole run. The sidebar is a persistent, window-level
checklist: the broker page stays fully visible (the browser reflows for it), it survives tab switches and navigations for free, and it lets us delete the fragile per-tab
re-injection loop (PING/PONG/reinjectIfMissing). Resolves Q-013; Q-015 confirmed empirically in Firefox 140 (one Start click drives the permission prompt, the sidebar open, and
the run start).
Built in reviewed slices — the full design and slice-by-slice status live in plan/sidebar-nav.md.
Architecture
renders and never re-derives.
open the entire broker list at once.
Sidebar views
Eight resting views derived from run + focus - no-run / guidance / verdict / challenge / offsite / revisit / done / stopped - plus transient saving/recorded owned by the UI
layer. Notably:
Message contract
SIDEBAR_GET_STATE · SIDEBAR_UPDATE {windowId} · VERDICT · DEFER · FOCUS_ITEM · NAVIGATE_BROKER_TAB · CHALLENGE_DETECTED/RESOLVED. Removed the dead
PING/PONG/GET_ITEM/REINJECT_OVERLAY machinery and the scripting + webNavigation permissions.
Correctness & data safety
Testing
npm run typecheck, npm test (165 tests), npm run build all green. Coverage 100% (pure logic fully covered; browser/DOM entrypoints excluded as integration-test TODOs). Manually
walked through in Firefox 140+.
Follow-ups (not in this PR)
Two pieces of the challenge-flag lifecycle cluster remain parked: load-time flicker and the loose per-tab flag. Neither is a data-loss bug. webNavigation.onErrorOccurred
(load-error view) is still deferred to M9.