feat(host,desktop): push a followed transcript's pages to the panels that draw it

Draft
CH
5d ago
+1495-11

First of three small PRs that remove Luke's last transcript and voice loops, stacked on #3311. The goal is realtime with no fallbacks. This one adds the host side: a transcript is pushed to the panels that follow it. The window still uses the held read; PR 2 moves it over.

What changed

  • Wire (coding-agent-view.ts): codingAgentTranscriptKeySchema names the transcript, either an agent's (kind: "agent", agentId, optional callId for one of its workers) or a task's planning worker's (kind: "taskWorker", taskId, callId). codingAgentTranscriptChangedSchema is { transcript, page }, where page is the existing CodingAgentMessagesAnswerView. Both are recorded in hosted-wire-schemas.test.ts, with goldens generated under LUKE_UPDATE_FIXTURES=1. The goldens are two new files and change nothing else.
  • Gateway: adds codingAgents.transcriptFollow/transcriptUnfollow (owned by the coding-agents composer), tasks.workerTranscriptFollow/workerTranscriptUnfollow (owned by the tasks composer), and a transcript.changed event.
  • Host (transcript-follow.ts):
    • followTranscript reads on the transcript's existing Realtime subscription (transcriptFollows) and tells every page as it reads it. The first page is read from 0:0. After a page with messages, the next is read right away. Otherwise the follow waits until the statements move, the subscription stops or opens again, or the service's wakeIn clock edge passes.
    • A seal that fails is asked for again after RESEAL, as before. A page read the service didn't answer is told and read again after RESEAL. Not found is told once and ends the follow.
    • Each composer keeps a FiberMap of follows. Following again starts over, and sign-out clears them.
  • Operator: transcriptFollow/transcriptUnfollow pick the method by kind; onTranscriptChanged reads the event.
  • Follow rows (packages/host/src/act-router/transcript-follow-acts.ts): these follow the inbox follow rows from #3264/#3306, and are generic over the panel. The desktop spreads them over its WebContents. Web-host spreads them over its tab and delivers through the in-process bridge's onTranscript, so a web tab follows transcripts the same way a window does.
    • It answers the codingAgents.transcriptFollow/Unfollow acts. A panel can hold several follows of one transcript, and they are counted.
    • The host lets a transcript go once no panel holds a follow, or when a panel's page is gone.
    • Each page goes to the panels that follow that transcript, on a new per-window app:transcript bridge channel.
    • Every follow asks the host to start from the beginning, so a panel that arrives late hears the whole transcript. Main stores none of the transcript's words.
  • Signed statements stay in the host. Tests check that no told page contains a capability.

Tests

  • compose-coding-agents.test.ts:
    • A followed agent transcript is told page by page: again when rows land, at the wakeIn edge, and when the statements move. Unfollowing stops it.
    • Not found is told once; behind a closed gate the follow is told unanswered; sign-out lets it go.
  • compose-tasks.test.ts: a planning worker's transcript is followed the same way.
  • in-process-bridge.test.ts: a page follows a transcript through the shell's follow rows and hears each page the host tells.
  • transcript-follow-acts.test.ts: pages are routed per panel, follows are counted, a page that goes away releases its follows, and only a panel can follow.

Checks run locally

pnpm check:affected gets OOM-killed in the sandbox, so I ran its pieces one at a time:

  • repository-checks.sh, biome check ., oxlint, knip
  • discarded-effect (with a larger heap)
  • tsc for features, gateway, host, panel, and desktop
  • vitest for host, features, gateway, and desktop main

What to check by hand

Nothing visible changes in this PR: no window sends the new acts yet. PR 2 is the visible one.

Made with Luke