Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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`.
20 changes: 20 additions & 0 deletions .docs/done/2026-06-27-one-click-capture-reliability/design.md
Original file line number Diff line number Diff line change
@@ -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).
17 changes: 17 additions & 0 deletions .docs/done/2026-06-27-one-click-capture-reliability/proposal.md
Original file line number Diff line number Diff line change
@@ -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.
42 changes: 42 additions & 0 deletions .docs/done/2026-06-27-one-click-capture-reliability/spec.md
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions .docs/done/2026-06-27-one-click-capture-reliability/tasks.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 4 additions & 2 deletions .docs/todo/2026-02-23-priority-todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 9 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<id>` (local-only page).
- `src/background.ts` — service worker; currently just an install log. `src/content.ts` — injected on `<all_urls>`; 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 `<all_urls>`; 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:<id>` keys; the large PNG _blob_ lives in IndexedDB (`shotback`/`shareImages`) keyed by `share-image:<id>`. `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=<id>")` — 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/<drive>/…` 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`).
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
Loading