From 05ee57274e3d28a8afd7ab3eae591f74dadfc133 Mon Sep 17 00:00:00 2001 From: DCCA Date: Sat, 27 Jun 2026 18:16:08 -0300 Subject: [PATCH] docs: document one-click capture, retries, and on-page notice - CLAUDE.md: content.ts message set (SB_CAPTURE_*), capture.ts retry helpers/guards (activateTab, sendToContentScript, isTabsBusyError, isNoReceiverError), the on-page notice flow, and wslPath/feedback helpers. - README: one-click capture, capture notice, and Copy for Claude Code in the feature list. - todo: drop the removed popup from the manual QA list; add one-click + notice QA items. - Add .docs/done/2026-06-27-one-click-capture-reliability/ change record (PRs #11-#14). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../completion-summary.md | 17 ++++++++ .../design.md | 20 +++++++++ .../proposal.md | 17 ++++++++ .../spec.md | 42 +++++++++++++++++++ .../tasks.md | 9 ++++ .docs/todo/2026-02-23-priority-todo.md | 6 ++- CLAUDE.md | 12 ++++-- README.md | 4 +- 8 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 .docs/done/2026-06-27-one-click-capture-reliability/completion-summary.md create mode 100644 .docs/done/2026-06-27-one-click-capture-reliability/design.md create mode 100644 .docs/done/2026-06-27-one-click-capture-reliability/proposal.md create mode 100644 .docs/done/2026-06-27-one-click-capture-reliability/spec.md create mode 100644 .docs/done/2026-06-27-one-click-capture-reliability/tasks.md diff --git a/.docs/done/2026-06-27-one-click-capture-reliability/completion-summary.md b/.docs/done/2026-06-27-one-click-capture-reliability/completion-summary.md new file mode 100644 index 0000000..86c74ba --- /dev/null +++ b/.docs/done/2026-06-27-one-click-capture-reliability/completion-summary.md @@ -0,0 +1,17 @@ +# Completion Summary: One-Click Capture Reliability + On-Page Notice + +Hardening of one-click capture, found while dogfooding. Shipped as four PRs. + +## What changed +- **Content-script retry (PR #11):** `sendToContentScript` re-injects `content.js` and retries the first `SB_GET_PAGE_METRICS` past the transient "Receiving end does not exist" error. Also fixes capturing tabs that were open before the extension was (re)loaded. +- **Tab-activation retry (PR #12):** `activateTab` retries `chrome.tabs.update(..., {active:true})` past "Tabs cannot be edited right now"; used for target activation and best-effort restore. +- **On-page notice (PR #13):** `content.ts` injects a fixed "Capturing full page… don't switch tabs or scroll" notice during capture, hidden for each frame and removed at the end. All notice messages are best-effort (`notify`). +- **Notice paint-race fix (PR #14):** `SB_SET_OVERLAY` now acks after the next paint (double-rAF) and capture adds a 60ms settle, so the notice no longer leaks into the screenshot. + +## Validation +- `tests/capture.test.ts`: `isNoReceiverError`, `isTabsBusyError`, and the retry/give-up/rethrow behavior of `sendToContentScript` + `activateTab`. **54 tests pass.** +- `npm run check` + `format:check` green; CI green before each merge. +- Confirmed the wiring in the built `dist/` (notice strings, double-rAF, retry guards). + +## Deferred +- Live in-browser confirmation (one-click → auto-capture → notice visible while scrolling → clean stitched PNG). Not runnable in CI; tracked in `.docs/todo/2026-02-23-priority-todo.md`. diff --git a/.docs/done/2026-06-27-one-click-capture-reliability/design.md b/.docs/done/2026-06-27-one-click-capture-reliability/design.md new file mode 100644 index 0000000..46c8dfd --- /dev/null +++ b/.docs/done/2026-06-27-one-click-capture-reliability/design.md @@ -0,0 +1,20 @@ +# Design: One-Click Capture Reliability + On-Page Notice + +All changes are in `src/lib/capture.ts` and `src/content.ts`; no manifest/permission changes. + +## Retry helpers (`src/lib/capture.ts`, exported + unit-tested) +- `isNoReceiverError(error)` / `isTabsBusyError(error)` — pure predicates matching the two transient Chrome error strings. +- `sendToContentScript(tabId, message, {retries=6, delayMs=150})` — send; on a no-receiver error, `ensureInjectable` (re-inject `content.js`) + `wait`, retry; rethrow other errors; throw after exhausting retries. Used for the racy first `SB_GET_PAGE_METRICS`. +- `activateTab(tabId, {retries=8, delayMs=150})` — `chrome.tabs.update(tabId, {active:true})`; on a tabs-busy error, `wait` + retry; rethrow others. Used to activate the target tab and (best-effort) restore the previously-active tab. + +## On-page notice +- `src/content.ts` builds a `position:fixed`, max-z-index, `pointer-events:none` notice with inline styles (no dependence on page CSS) and a CSS-keyframe spinner. Handlers: `SB_CAPTURE_BEGIN` (show), `SB_SET_OVERLAY {visible}` (toggle), `SB_CAPTURE_END` (remove). `SB_SCROLL_TO` re-shows it; `SB_RESTORE_SCROLL` removes it. +- **Paint correctness:** `SB_SET_OVERLAY` acks via `afterPaint` = double `requestAnimationFrame`, so the display change is painted before the orchestrator proceeds — a single rAF runs before paint and let the notice leak into a frame. +- `src/lib/capture.ts` drives the lifecycle: `notify(SB_CAPTURE_BEGIN)` + a 450ms read pause; per segment `SB_SCROLL_TO` (re-show) → `wait(120)` → `notify(SB_SET_OVERLAY false)` → `wait(60)` (compositor settle) → `captureVisibleTab`; `SB_RESTORE_SCROLL` + `notify(SB_CAPTURE_END)` in `finally`. `notify` swallows errors so the cosmetic notice can never abort a capture. + +## Testing +- `tests/capture.test.ts` covers `isNoReceiverError`, `isTabsBusyError`, and the retry/give-up/immediate-rethrow paths of `sendToContentScript` and `activateTab` with a stubbed `globalThis.chrome` (54 tests total). +- The DOM overlay + capture orchestration are at the `chrome.*`/DOM boundary → verified manually (load unpacked), per the repo's no-runner-for-live-extension convention. + +## Delivery +Shipped as four PRs off `main`: #11 (content-script retry), #12 (tab-activation retry), #13 (on-page notice), #14 (notice paint-race fix). diff --git a/.docs/done/2026-06-27-one-click-capture-reliability/proposal.md b/.docs/done/2026-06-27-one-click-capture-reliability/proposal.md new file mode 100644 index 0000000..24fc41d --- /dev/null +++ b/.docs/done/2026-06-27-one-click-capture-reliability/proposal.md @@ -0,0 +1,17 @@ +# Proposal: One-Click Capture Reliability + On-Page Notice + +## Why +One-click capture (`.docs/done/2026-06-27-claude-code-capture-handoff/`, Change A) fires the instant the editor opens. Dogfooding surfaced three problems the manual Capture button never hit, because it runs seconds later once the browser has settled: + +1. **`Could not establish connection. Receiving end does not exist.`** — the target tab's content script had not registered its message listener yet when `SB_GET_PAGE_METRICS` was sent. +2. **`Tabs cannot be edited right now (user may be dragging a tab).`** — `chrome.tabs.update(target, {active:true})` ran while the just-opened editor tab was still being inserted into the tab strip. +3. **No on-screen feedback.** Capture activates and scrolls the _target_ tab, so the user is looking at the page (not the editor's progress text) and could switch tabs / scroll and corrupt the stitch — with nothing telling them not to. The first cut of the notice also leaked into the screenshot. + +## Scope +- Make the one-click auto-capture path resilient to both transient Chrome errors via inject-and-retry / activate-and-retry helpers. +- Show an on-page "Capturing…" notice during capture, guaranteed out of the captured frames. + +## Out of Scope +- Changing the capture algorithm (scroll/stitch) itself. +- New permissions (none required). +- Configurable notice text/position. diff --git a/.docs/done/2026-06-27-one-click-capture-reliability/spec.md b/.docs/done/2026-06-27-one-click-capture-reliability/spec.md new file mode 100644 index 0000000..24ed3ff --- /dev/null +++ b/.docs/done/2026-06-27-one-click-capture-reliability/spec.md @@ -0,0 +1,42 @@ +# Spec: One-Click Capture Reliability + On-Page Notice + +### Requirement: Resilient Content-Script Messaging +Capture MUST retry the initial content-script message while the receiving end is not ready, re-injecting the content script between attempts. + +#### Scenario: Receiver not ready on auto-capture +- GIVEN one-click capture fires before the content script has registered its listener +- WHEN `SB_GET_PAGE_METRICS` is sent and rejects with "Receiving end does not exist" +- THEN the content script is re-injected and the message retried up to a bounded number of times +- AND capture proceeds once a response is received + +#### Scenario: Unrelated error is not retried +- GIVEN a message rejects with an error that is not a no-receiver error +- WHEN the send fails +- THEN it is rethrown immediately without retrying + +### Requirement: Resilient Tab Activation +Capture MUST retry activating the target tab while the tab strip is transiently locked. + +#### Scenario: Tab strip busy on auto-capture +- GIVEN the editor tab was just created and the strip is still settling +- WHEN `chrome.tabs.update(target, {active:true})` rejects with "Tabs cannot be edited right now" +- THEN activation is retried with a short backoff until it succeeds or retries are exhausted + +### Requirement: On-Page Capture Notice +The page being captured SHALL display a notice telling the user not to switch tabs or scroll while capture runs, and the notice MUST NOT appear in the saved screenshot. + +#### Scenario: Notice visible during capture +- GIVEN a full-page capture is running +- WHEN the page is scrolling between segments +- THEN a fixed notice ("Capturing full page… don't switch tabs or scroll") is visible on the page + +#### Scenario: Notice excluded from frames +- GIVEN the notice is shown +- WHEN each `captureVisibleTab` frame is taken +- THEN the notice is hidden and the hide has been painted before the frame is captured +- AND the notice is removed when capture finishes or is restored + +#### Scenario: Notice failure never breaks capture +- GIVEN the notice messages cannot be delivered (e.g. an old content script) +- WHEN capture runs +- THEN the capture still completes normally diff --git a/.docs/done/2026-06-27-one-click-capture-reliability/tasks.md b/.docs/done/2026-06-27-one-click-capture-reliability/tasks.md new file mode 100644 index 0000000..567a2ad --- /dev/null +++ b/.docs/done/2026-06-27-one-click-capture-reliability/tasks.md @@ -0,0 +1,9 @@ +# Tasks: One-Click Capture Reliability + On-Page Notice + +- [x] 1. `sendToContentScript` + `isNoReceiverError`; retry the metrics call (PR #11). +- [x] 2. `activateTab` + `isTabsBusyError`; use for target activation + best-effort restore (PR #12). +- [x] 3. On-page notice in `content.ts` + lifecycle in `capture.ts` (PR #13). +- [x] 4. Fix notice leaking into frames via double-rAF + settle (PR #14). +- [x] 5. Unit tests for all four helpers/guards (`tests/capture.test.ts`, 54 passing). +- [x] 6. Docs updated (CLAUDE.md architecture, README features, todo QA list). +- [ ] 7. Manual QA after reload: one-click auto-captures; notice visible while scrolling and absent from the saved PNG. diff --git a/.docs/todo/2026-02-23-priority-todo.md b/.docs/todo/2026-02-23-priority-todo.md index 694a277..948785d 100644 --- a/.docs/todo/2026-02-23-priority-todo.md +++ b/.docs/todo/2026-02-23-priority-todo.md @@ -22,8 +22,10 @@ ## 3. UI Refinement Follow-ups -- [ ] Run manual QA pass across popup/editor/viewer (not runnable in CI): - - popup sizing on different Chrome zoom levels +- [ ] Run manual QA pass across editor/viewer (not runnable in CI; the popup was + removed in favor of one-click capture): + - one-click capture: toolbar icon opens the editor and auto-captures + - on-page capture notice is visible while scrolling and absent from the saved PNG - editor timeline readability and button hierarchy - viewer metadata readability on narrow widths diff --git a/CLAUDE.md b/CLAUDE.md index aa195f9..e6e0bd6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,16 +27,22 @@ A Manifest V3 Chrome extension (TypeScript + React 18 + Vite + Tailwind). Two HT - The toolbar icon has **no popup**: `src/background.ts`'s `chrome.action.onClicked` handler opens the editor in a new tab with `?tabId=&windowId=&autocapture=1` of the active tab, and the editor auto-captures once on load (one-click capture). - `src/editor/main.tsx` — the heart of the app (~1000 lines). Drives capture, hosts the annotation canvas, the comment timeline, general feedback, and the three output actions. - `src/viewer/` — renders a saved share from `?share=` (local-only page). -- `src/background.ts` — service worker; currently just an install log. `src/content.ts` — injected on ``; responds to `SB_GET_PAGE_METRICS` / `SB_SCROLL_TO` / `SB_RESTORE_SCROLL` messages. +- `src/background.ts` — service worker; logs on install and hosts the `chrome.action.onClicked` handler (one-click capture). `src/content.ts` — injected on ``; responds to `SB_GET_PAGE_METRICS` / `SB_SCROLL_TO` / `SB_RESTORE_SCROLL`, and to the capture-notice messages `SB_CAPTURE_BEGIN` / `SB_SET_OVERLAY` / `SB_CAPTURE_END`. -**Full-page capture flow** (`src/lib/capture.ts`, called from the editor — _not_ from the background): activates the target tab → `ensureInjectable` re-injects `content.js` → reads page metrics → `buildScrollSteps` computes viewport-sized scroll offsets → for each step, scrolls the page (via content-script message) and calls `chrome.tabs.captureVisibleTab` → stitches the PNG segments onto a single canvas scaled by `devicePixelRatio`. Restores scroll and the previously-active tab in `finally` blocks. +**Full-page capture flow** (`src/lib/capture.ts`, called from the editor — _not_ from the background): `activateTab` brings the target tab forward → `ensureInjectable` re-injects `content.js` → reads page metrics → `buildScrollSteps` computes viewport-sized scroll offsets → for each step, scrolls the page (via content-script message) and calls `chrome.tabs.captureVisibleTab` → stitches the PNG segments onto a single canvas scaled by `devicePixelRatio`. Restores scroll and the previously-active tab in `finally` blocks. + +**One-click auto-capture races two transient Chrome errors** (it fires the instant the editor opens, before the tab strip/content script have settled), handled by retry helpers in `capture.ts`: `activateTab` retries `chrome.tabs.update` past `isTabsBusyError` ("Tabs cannot be edited right now"), and `sendToContentScript` re-injects + retries past `isNoReceiverError` ("Receiving end does not exist"). All four are exported and unit-tested in `tests/capture.test.ts`. + +**On-page capture notice:** because capture activates and scrolls the _target_ tab (the user is looking at the page, not the editor), `content.ts` injects a fixed top-of-viewport notice ("Capturing full page… don't switch tabs or scroll"). The orchestrator shows it (`SB_CAPTURE_BEGIN`), hides it for each `captureVisibleTab` frame (`SB_SET_OVERLAY`, acked only **after the next paint** via double-rAF so it never leaks into a frame), re-shows it on each `SB_SCROLL_TO`, and removes it on `SB_RESTORE_SCROLL` / `SB_CAPTURE_END`. Notice messages go through `notify` — best-effort, never abort a capture. **Storage (two-tier, see `src/lib/localStore.ts` + `src/lib/shareDb.ts`):** share _metadata_ (annotations, feedback, page URL, blob key) lives in `chrome.storage.local` under `share:` keys; the large PNG _blob_ lives in IndexedDB (`shotback`/`shareImages`) keyed by `share-image:`. `localStore` is the only module that touches both; it converts dataURL↔Blob, enforces `schemaVersion: 2`, transparently migrates legacy v1 records (inline `imageDataUrl`) on read, and prunes via `DEFAULT_RETENTION_POLICY` (50 shares / 30 days) after each save. A share link is `chrome.runtime.getURL("viewer.html?share=")` — intentionally profile-scoped, never a public URL. **Pure, unit-tested helpers** (these are where the real logic and the tests live — `tests/*.test.ts` mirror them): - `src/lib/annotate.ts` — `exportAnnotatedImage` rasterizes annotations onto the screenshot; `selectFeedbackRenderMode` picks footer vs. overlay so the export canvas never exceeds `MAX_EXPORT_CANVAS_HEIGHT`/`AREA` limits. -- `src/lib/feedback.ts` — `buildExternalLlmPrompt` (the structured prompt copied for the cloud-LLM fallback) and `annotationSummary`. +- `src/lib/feedback.ts` — `buildExternalLlmPrompt` / `buildClaudeCodePrompt` (the structured prompts copied for the cloud-LLM and Claude Code exports) and `annotationSummary`. +- `src/lib/capture.ts` — `buildScrollSteps` (scroll offsets) plus the retry helpers/guards `activateTab`, `sendToContentScript`, `isTabsBusyError`, `isNoReceiverError`. +- `src/lib/wslPath.ts` — `toClaudePath` translates a Windows path to its WSL `/mnt//…` mount for the Claude Code export. - `src/lib/boxResize.ts` — box drag/resize geometry. - `src/lib/selectNavigation.ts` — `getNextIndex`/`matchTypeahead` keyboard-navigation rules for the custom `Select` listbox (so the interaction logic is testable apart from the DOM). - `src/types/annotation.ts` — the `Annotation` discriminated union (`box` | `arrow` | `text`). diff --git a/README.md b/README.md index fe97525..61ede3f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ This keeps human feedback and AI review grounded in the same visual evidence. ## ✨ Features -- 📷 **Full-page capture** (`scroll + stitch`) +- ⚡ **One-click capture** — clicking the toolbar icon opens the editor and captures immediately (no popup, no second click) +- 📷 **Full-page capture** (`scroll + stitch`) with an on-page "Capturing…" notice so you know not to switch tabs or scroll - ✏️ **Area annotations**: box, arrow, text - 🔗 **Linked comments** tied to selected annotation - ⏱️ **Comment timeline** with per-item remove @@ -40,6 +41,7 @@ This keeps human feedback and AI review grounded in the same visual evidence. - 🤖 **External LLM fallback**: - downloads annotated image - copies a structured prompt to clipboard +- 🧑‍💻 **Copy for Claude Code** — saves the PNG to `Downloads/shotback/` and copies a prompt referencing it by path (Windows → WSL `/mnt/c/...` translation) so a Claude Code session can read it directly ## 🚀 Quick Start