Skip to content

The cloud session mirror reads the page's turns, not a page dump (#1225) - #1671

Merged
suleimansh merged 1 commit into
mainfrom
mirror-per-turn
Aug 23, 2026
Merged

The cloud session mirror reads the page's turns, not a page dump (#1225)#1671
suleimansh merged 1 commit into
mainfrom
mirror-per-turn

Conversation

@suleimansh

Copy link
Copy Markdown
Contributor

🤖 automated

Addresses #1225 — item 1 ("it dumps the whole page, not the conversation"); item 2 (the 8000-char cap cutting from the front) goes with it, since the cap is now per turn.

What was wrong

The content script looked for <article> elements to split the conversation. claude.ai renders none (articles 0 in every hello note), so the mirror always fell to its plan B: the whole conversation region as one 8000-character tail. What the dashboard showed was the run's own prompt, the sidebar labels, and the actual exchange somewhere at the end, with no turn boundaries.

What the page actually marks

Inspected on a real session (session_01HgamNnpzgVyp8TUoWt7fRM): the transcript is a div[role="feed"] ("Chat messages") of div[data-testid="transcript-row"], one per turn, each carrying data-index (position), data-perf-row (human / assistant / marker) and data-perf-row-streaming. Row 0 is the run prompt (11.5 k chars); the exchange is rows 2–5. The list is virtual — a long session keeps only its tail rendered.

What changes

Extension 0.9.0 → 0.10.0 (packages/chrome-extension, daemon lockstep bumped)

  • transcript() reads the rows: seq = data-index, human → user, assistant → agent, markers skipped; per-turn text cleaned (icon glyphs, blank lines) and head-capped at 8000 — only the prompt turn is ever that long. Streaming replies are re-sent in place (the store already keys by position).
  • The page-dump fallback is removed. A page with no turn rows mirrors nothing; the panel's transcript line and the hello note say no transcript rows found. A layout change is a named state, not 8 k of sidebar that reads like output.
  • The hello note reports the row kinds seen (rows 8 (human,marker,assistant), turns 4), so a new kind shows up by name.
  • Same selector fixes a latent gap: findPendingChoice scoped "the opening message" by deepQueryAll('article')[0]undefined on every live page, so the Bridge false-positive: the system-prompt template renders as a real question when the session asked nothing #1568 decoy exclusion by position never ran there and isTemplate carried it alone. The opening message is now the data-index="0" row.
  • check.mjs: fixtures move to the real row shape; three new cases — one entry per turn with roles mapped / markers skipped / prompt head-capped; positions from the page when only the tail is rendered; no rows → nothing mirrored and the panel says so. Break-checked (positions from DOM order fails).

Dashboard (CloudAgentNotice.tsx)

  • Turns render by role: the user's as one muted you › line (first line, whole turn on hover — the opening turn is the entire prompt), the session's in full; the box scrolls to the newest turn whenever it changes or grows. scrubMirrorText stays (per-message action labels still ride in row text). One new test, break-checked.

No change to /_bridge/events or the store — the seq/role/text contract already fit.

SPEC.md changes

packages/chrome-extension/content.SPEC.md — the page half of the bridge.

TL;DR, two lines replaced:

-- **The transcript is mirrored per message** - each message block is reported under its position, and only positions whose text changed are re-sent.
-- **When the page has no message blocks, the conversation is mirrored whole** - as one block holding its most recent text, rather than guessing where messages divide.
+- **The transcript is mirrored one turn at a time** - each conversation turn the page marks is reported under the position the page gives it, as the user's or the session's, and only turns whose text changed are re-sent.
+- **A page that marks no turns mirrors nothing** - the panel and the self-report name that state, rather than mirroring whatever text is on screen.

"Mirroring the transcript" rewritten (rows, kinds, positions from the page, streaming replace, nothing-when-no-rows, what the self-report carries; the rationale records why the dump was dropped). The "Mirroring a page with no message blocks" section is removed. In "Telling a real question from the protocol's own examples", the position rule now names the opening turn as position zero and says what happens when a virtual list has scrolled it away. The panel's "how many message blocks" → "how many turn rows".

packages/chrome-extension/SPEC.md — one phrase: the transcript is mirrored "turn by turn, as it is written".

packages/chrome-extension/check.SPEC.md — the #1568 bullet speaks of the opening turn; a new "Mirroring the transcript" group lists the three cases above.

packages/framework/dashboard/components/CloudAgentNotice.SPEC.md — TL;DR and glossary say "turn by turn"; the mirror section gains:

+Each turn is its own block, in transcript order. The user's turns show as a single line — "you ›" followed by the turn's first line, the whole turn available on hover — because the opening turn is the run's entire prompt and what the session did is the point of the mirror, not what it was told. The session's turns are shown whole. The box keeps its newest turn in view, scrolling to the end whenever the last turn changes or grows, so a reply being written is watched rather than scrolled to.

FEATURES-SPEC.md — the mirror had no entry; added under Remote execution and sharing:

+- A cloud session's conversation mirrored into the run view, turn by turn, as it is written

Verified

  • node check.mjs: 20/20. pnpm build / typecheck / test: 1581 daemon + 804 dashboard, green.
  • Dogfood on a live gemstack web run: pending the extension reload (human step) — will be reported here.

claude.ai renders no <article> elements, so the mirror never found a message and fell to its
plan B: the whole conversation region as one 8000-character tail — the run's prompt, the
navigation labels and the exchange somewhere at the end, with no boundaries.

The page does mark its turns: one `transcript-row` per turn in a `role="feed"`, each naming its
position (`data-index`) and kind (`data-perf-row`: human / assistant / marker). The content
script now reads those: one entry per user or session turn under the page's own position,
markers skipped, text head-capped per turn (only the prompt turn is ever that long), streaming
replies re-sent in place. The page-dump fallback is gone — a page with no turn rows mirrors
nothing and says so, rather than mirroring whatever is on screen. The same row is now the
"opening message" the #1568 decoy exclusion scopes by, which on a live page it never found.

The dashboard renders by role: the user's turns as one "you ›" line (the opening turn is the
whole prompt), the session's in full, the newest kept in view. Extension 0.10.0, daemon lockstep
bumped; check.mjs fixtures move to the real row shape and cover the mirror.

Addresses #1225 (item 1; item 2, the front cut, goes with it).
@suleimansh

Copy link
Copy Markdown
Contributor Author

🤖 automated

Dogfooded end to end on a live Opus gemstack web run (2026-08-23T19-49-17-177Z / session_01J221xfDeFa5MNm2Rvaaf4E), extension 0.10.0 reloaded:

  • Hello note on the live page: rows 4 (human,marker,assistant), turns 2 — the real row kinds, named.
  • Mirrored turns at the daemon: (0 user 8000) — the prompt, head-capped — then (2 agent), (3 user) — the answer the bridge typed — (4 agent). Positions are the page's own (seq 1, a marker, is absent by design), markers skipped, roles correct.
  • The parked question crossed whole (recommended + stop options), was answered from the dashboard, typed back, and the session resumed — the [CC web driver] Add support for choices? For full-fledged CC web driver #1554 round trip is unaffected.
  • Run view: 4 alternating blocks; both you › turns render as one truncated line with the full turn in the tooltip, both session turns in full, Show message actions scrubbed, box scrolled to the newest turn.
  • The session created NOTES.md with the chosen marker, committed, pushed claude/dogfood-per-turn-mirror-r2jf50 — branch verified and deleted, along with the run's empty tf-agent-* branch.

@suleimansh
suleimansh merged commit 35dca7c into main Aug 23, 2026
3 checks passed
@suleimansh
suleimansh deleted the mirror-per-turn branch August 23, 2026 22:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant