Repository navigation
feat(freshclaude): live transcript — streamed text, tool rows, retries, notices and subagent activity while a turn runs - #871
Merged
Conversation
The assistant, stream-event and compaction frames move out of handleSdkMessage into sdk-message-frames.mjs (framesForSdkMessage, one function per kind), next to core's result mapping. index.mjs keeps the kinds with side effects (init, commands_changed, result, the checkpoint replay binding). New characterization tests feed the pinned CLI's recorded SDK sequences through the mapping and pin today's frames. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… carries its ids (B2)
The CLI writes every assistant and user message, compaction boundary and
local command output to its transcript under the message's own uuid,
about 100 ms after emitting it. The sidecar now follows each such message
with sdk.session.changed {reason:'transcript', uuid, parentToolUseId},
which the client already treats as a snapshot invalidation, so a visible
pane refetches per message instead of waiting for the 3 s busy poll.
sdk.assistant also carries uuid, messageId and parentToolUseId, so a live
streamed block can be bound to the transcript line it becomes.
Tests: the mapping over every recorded real-CLI sequence announces exactly
the messages whose uuid is a transcript line (main or subagent); through
index.mjs over the simulator, each announced uuid lands in the transcript
within 2 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ith its message id (B3) sdk.stream now carries the main thread's message_start, block starts and stops, and text/thinking deltas only, each stamped with the API message id of the message being streamed (from its message_start). A subagent's events, tool input JSON, signatures, message_delta and message_stop are no longer sent. The id equals the messageId of the sdk.assistant each block becomes, which is what binds a live block to its transcript line. The CLI simulator now gives each assistant message the id of the message_start it was streamed under, as the real CLI does (F2); a normalized conformance trace cannot see ids, so a simulator test pins it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ers reach the server (B4)
The sidecar used to drop these SDK messages. Now:
- system/api_retry becomes sdk.retry {attempt, maxRetries, delayMs,
errorStatus, error};
- rate_limit_event becomes sdk.notice RATE_LIMIT_WARNING (allowed_warning)
or RATE_LIMITED (rejected) carrying the limit and its reset time; an
allowed status says nothing;
- system/informational becomes sdk.notice INFORMATIONAL with its text.
None of them is a turn end or an error frame. Each is logged on stderr
(api.retry at warn; notice at its level) with ids and numbers only: an
informational text is logged by its size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sion mode (B5)
A status frame that ends a compaction (status:null with compact_result)
used to be dropped, so a pane kept saying "compacting" until the turn's
result and a failed compaction left no trace but the transcript line. Now:
- a failed compaction is sdk.notice COMPACTION_FAILED (level error) with
the CLI's reason, never an error frame or a turn end;
- while a send still waits for its result, the end of a compaction sends
sdk.status running (interim until core's turn ledger, core plan N2).
When the CLI reports a permission mode that differs from the session's
(after an ExitPlanMode approval), the session adopts it and says so in
sdk.session.metadata {permissionMode}; a later configure to that mode
therefore calls nothing on the CLI.
The simulator records each settings call it handles (ledger kind
`control`), which is how the spawn-level test sees that no redundant
setPermissionMode was made.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-live-frames runs the production sidecar over the pinned SDK 0.3.237 and CLI 2.1.237 against the local provider (never the real API): text with a thinking block, Write then Edit, a synchronous Agent whose subagent reads a file, two 529 retries, a successful and a failed /compact, and an ExitPlanMode approval. It pins the stream filter and its message ids, the transcript announcements (each announced uuid is on disk within 2 s, subagent ones in the subagent file), sdk.retry with its log records, the compaction outcome frames, and the permission-mode fold. Against the pre-Phase-B sidecar, 8 of its 10 tests fail. Real-CLI fact it pins: approving ExitPlanMode returns the CLI to the mode it had before plan mode (here bypassPermissions), not always default. startLocalProvider gains an optional `respond` hook (a test's own model, asked before the prompt markers), so the shared marker language the simulator conforms to is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sidecar's sdk.retry and sdk.notice frames were dropped by the sdk.* -> freshAgent.* rename table. They now broadcast as freshAgent.retry and freshAgent.notice, stamped with the pane's id like every other sidecar event, and never end the turn or change its status. Each is also logged under the pane's id (freshagent.claude.api_retry at info, freshagent.claude.notice at warn or info by its level), with ids, numbers and codes only: a notice's text is logged by its size. A transcript announcement (sdk.session.changed reason 'transcript') already passed through; a test now pins that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n's setting (C1)
After an approved ExitPlanMode the CLI leaves plan mode by itself and the
sidecar reports sdk.session.metadata {permissionMode}. The stdout
consumer now folds that mode into the session's settings before the
frame is broadcast, so a later configure or settings-bearing send that
states the same mode writes nothing to the sidecar, and the snapshot's
live settings overlay states the mode the CLI runs in. The change is
logged as freshagent.claude.permission_mode_changed under the pane's id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
session_metadata_frame takes an optional permission mode, stated only when given; codex and opencode pass None, so their frames are unchanged. Claude's configure and settings-bearing send now diff the session's model, effort and permission mode around the apply, and broadcast a metadata frame stating all three when any changed. A configure that only switches into plan mode (or a send from a device that missed it) now converges every device's settings surfaces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two program steps for the CLI simulator, so rigs over the real sidecar can run rich turns without the real CLI: - replay plays the model's side of a turn the pinned CLI 2.1.237 really ran (the real-cli-2.1.237 recordings): its SDK messages with the recorded spacing, each persisted line into the session's transcript on the simulator's ~100 ms flush (F7), and each subagent's transcript and meta.json (written when its Agent call starts, F5). Every recorded id is minted afresh per replay and the recording's paths become the session's. holdBeforeMs keeps the turn before a recorded message (a slow tool), so a test can look at the session mid-turn. An interrupt during a replay fails the stream loudly (not modeled). - api_retry fails the model request N times with a status: one system/api_retry per attempt with the SDK's fields (F11), then the CLI's backoff, then the next step. Self-tests compare a replay with its recording (messages, transcript lines and subagent files, ids and paths masked), check a hold leaves the subagent's tool call on disk and running, and pin the api_retry shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…write The snapshot route and the session host's capture read transcripts from this process's CLAUDE_CONFIG_DIR/HOME, while a lane whose sidecars get an explicit environment (the RealSidecarSim/RealSidecarCli rigs) has its CLI write under that environment's store, so a rig's snapshot answered 404 for a live session. The lane now names its store (FreshClaudeState::transcript_store_roots: the sidecar environment's roots when one is configured, else this process's) and every snapshot read goes through FreshClaudeState::transcript_snapshot. Production, which configures no sidecar environment, reads the same store as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New freshagent_claude_live_rendering suite on the RealSidecarSim rig, which now also serves the snapshot REST route (ClaudeRig::snapshot): - a replayed real-CLI turn (Write, Edits, Read, a synchronous subagent) streams frames that each name their API message, assistant frames that name their transcript line, and exactly one transcript announcement per persisted message, every one of which lands on disk; - mid-turn, the REST snapshot already shows the running delegation and its subagent's running Read; after the turn it shows the completed delegation, the recording's changed files and its token usage; - two overloaded attempts arrive as freshAgent.retry frames before the reply; an approved ExitPlanMode reaches the client as metadata (permissionMode default) and the snapshot's live settings follow; a failed /compact is a COMPACTION_FAILED notice with no error; a configure back to plan broadcasts metadata stating it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nce the last read (C4) A live pane now refetches after every message the CLI persists, on every connected device, and each refetch re-read and re-projected the whole transcript. A process-wide cache (claude_projection_cache, 8 transcripts, least recently read dropped) keeps each transcript's projector, the offset it consumed (always after a newline) and the 4 KiB before it; a read parses only the complete lines after that offset. A file that is new to the cache, shorter than the offset, rewritten before it, or replaced by another file (inode) is parsed in full. An unterminated last line is never consumed: a tail that already parses counts for that read only, so every read equals a full parse of the file as it stands. Subagent activity is still attached per read, so a growing subagent transcript shows while the main one is unchanged. Reads stay on spawn_blocking; each is logged at debug with what it parsed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tests can now build a store with the app's exact state shape (and so the exact selector inputs every component sees) without the production store's module-level side effects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s map (D1) Token-rate state leaves state.freshAgent.sessions (transcript-live P3). freshAgent.stream frames become typed ops (block start, text/thinking delta) that a 100 ms batcher applies to the new freshAgentLive slice; any other event of the session flushes its pending ops first, so the store sees them in arrival order. The first block of a turn marks the session streaming once (streamOpened); later blocks and tokens write nothing there. Removed as dead (nothing rendered them): streamingText, appendStreamDelta, addAssistantMessage, addUserMessage, clearStreaming and the per-block setStreaming(false). An assistant frame no longer invents a client-side turn; durable content comes from the snapshot (P1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st (D2) TabBar, PaneContainer, Sidebar, MobileTabStrip, TabSwitcher and ContextMenuProvider, rendered against the app's reducer map, are fed 200 real freshAgent.stream frames through the websocket handler, each in its own act. Before D1 each committed once per token (PaneContainer twice); now none commits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sessionExited resolved a key from the full locator and wrote the status of a session record that did not exist, so the reducer threw inside the websocket handler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sion (D3) - assistantBlockBound gives the first unbound streamed block of the same API message and kind its transcript line uuid (the message's full text or tool input wins over the stream); subagent messages bind nothing. - transcriptAnnounced records each main-thread line the CLI will write; transcriptLanded removes the blocks and expectations a snapshot holds; transcriptExpectationsDropped ends the re-poll for lines that never land. - liveRetrySet shows an API retry (freshAgent.retry) until the next block starts; it never touches the session (P4). - The tail moves with materializeSession and clears on exit, lost, kill, status exited/stuck and a non-restore error (plan N5); idle does not clear it, so the text stays until its lines land. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… it becomes (D4) FreshAgentLiveTail selects only its own session's tail and builds one synthetic assistant turn from the blocks whose transcript line the rendered snapshot does not hold yet, plus the API retry in progress (last, so it is the strip's live row). It renders through the transcript's own layout and turn article: streaming markdown text, the live thinking strip, a tool row named at block start (its input preview once the message carries it) and the Retrying reel look exactly like durable content. FreshAgentTranscript gains a tail slot after the turns and a TranscriptFollowContext: the tail asks to follow on growth and the transcript pins to the bottom only while the reader is there. A live article has no turn actions, no turn index or gesture ownership, and is aria-busy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FreshAgentView mounts the live tail in the transcript's tail slot and never subscribes to its text, only to whether it shows anything: while it does, the transcript's own live strip yields, so one live reel shows. useTranscriptLandings runs on every committed snapshot. It lands the blocks and announced lines the snapshot holds (its turn ids plus the server's transcriptLineUuids); an announced line still missing earns a re-poll, at most LANDING_RECHECK_MAX = 4 per line, after which it is dropped with a warn log. Once nothing is awaited and the session is not busy, the tail clears: after its lines are on screen, so the text never blinks out between the result and the snapshot (the plan cleared on any idle commit). A turn that ends with live content left asks for one more snapshot. Re-polls skip hidden panes. Tests: a transcript announcement refetches within the debounce; the bounded re-poll; streamed text shown exactly once while streaming, after landing and after the turn; an unlanded block outlives the result until its line lands; one live reel; and a profiler proves the view commits nothing across 200 tokens (the tail stubbed out). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…out waiting for another render The pane's memoized content did not list the live-tail values among its dependencies, so when the tail started showing content with nothing else changing, the transcript kept its own live strip and two reels showed. The one-reel test now lets every unrelated refresh settle first, so the tail must take over on its own; it fails without the dependencies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nding the turn (D6)
freshAgent.notice (a rate limit near or reached, a failed compaction, an
informational CLI banner) folds into the live slice with a sequence number
and never touches the session (P4). The pane shows each new notice once in
its notice banner (role alert); a rate-limit notice adds when the limit
resets, in the viewer's local time ('3:45 PM', or 'Fri 3:45 PM' on another
day; the CLI's epoch seconds, or milliseconds). A malformed notice is
dropped with a warn log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tates (D7) freshAgent.session.metadata may now state permissionMode (absent = no statement, as for model and effort); the session record keeps it. When the stated mode changes — the CLI left plan mode after an approved ExitPlanMode, or another device changed it — the pane adopts it, so the gear shows it and the next send keeps the CLI there instead of reverting it. Only a change of the stated mode acts, so a mode the user just picked in the pane is never undone; a mode outside the pane's known set is kept out with a warn log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… adding to it (D8) The SDK's total_cost_usd on each result already covers the whole session, so summing it counted every earlier turn again. Token usage is per turn and still adds up; a result without a cost keeps the last known total. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l hides once it lands A live block was hidden only by the line uuid its assistant frame names. When the snapshot holding the line outran that frame, or a reconnect lost it, the block stayed beside its durable turn and the text showed twice. The CLI writes one line per content block of a message, in block order (real-CLI fact F2), and snapshot turns carry their API message id, so an unbound block now lands when the line at its message position holds the same kind of block. A bound block is still matched by its uuid alone. fresh-agent-landed-transcript.ts derives this once per committed snapshot and is shared by the reducer, the tail and the pane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… by frame (D9) A jsdom flow over the app's real store, the real websocket handler and batcher and the real pane, with only the snapshot API mocked. A React profiler counts the answer on screen after every commit: from its first token through the landed transcript and the turn's end it is there exactly once, also when its line lands only after the result. The first case found the unbound-block duplicate fixed in the previous commit; clearing the tail before its lines land fails the second (a gap frame). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… position Replays the pinned CLI's recorded tools-sync turn through the real sidecar mapper and the websocket handler, then lands it against the snapshot the server projects from the transcript the CLI wrote: every streamed main-thread block binds to its line, and every block lands once the snapshot holds the turn, also when every assistant frame was lost. It pins the order the position match relies on (one line per content block, in block order, F2) to real data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pane in a background tab kept re-rendering its tail's markdown ten times a second while its agent streamed. The tail now mounts only while the pane is visible; its state stays in the store, so revealing the pane shows the tail as it stands (after the usual hidden-pane refresh). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he recorded tools did The replay step gains `workspace: true`: each successful recorded Write or Edit result writes the file it reports (Write's content, Edit's edit of the file it read) before the result is reported, and only inside the session's cwd. Browser tests need it to see a real `git diff` behind the changed-files panel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sidecar (E1, E2) A new browser lane (helpers/claude-sim-lane.ts) boots a spec-owned Rust server whose claude sidecar is the production index.mjs over the CLI simulator, with a simulator program per lane, and opens a freshclaude or kilroy pane on it. A per-frame text census records what the pane showed. freshclaude-live-transcript-rust.spec.ts drives the plan's cases 1-9 (and the kilroy variant of 1, 4 and 5): streamed text while the turn runs, on screen exactly once with no gap; a tool row before the turn ends; the Retrying row; /compact success and failure (notice plus durable command error); a running subagent's activity rows; the changed-files panel and its git diff; /cost; plan exit converging the gear and the next send; a reload mid-session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Freshclaude tab with two panes: one mid-turn, with its activity rows, a subagent's delegation and its ↳ rows, the changed files, and the answer streaming into the live response; the other waiting on an API retry with a "Retrying (attempt 2) — overloaded (529)" row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`inputStreamMs` on a `tool` step spreads the call's input_json_delta pieces over that long before the assistant message completes the call, as the model does for a long Write. Without it a tool step behaves as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Case 2 now streams a long Write's input for 4 s: the live response shows the Write call before any of the turn is in the transcript (only the live tail can do that), then the running Bash call shows before the turn ends. A lane program may be a function of the pane's workspace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hind EchoSession pointed FRESHELL_CLAUDE_SIDECAR and CLAUDE_CONFIG_DIR at its tempdir and never removed them, and the snapshot-store test removed its CLAUDE_CONFIG_DIR only when its assertion passed. Both now reset the variables on drop, as EndingSession does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client drops a freshAgent.stream frame that names a parent tool use; no test held it. Deleting the guard now fails this routing test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…notice Every system/informational message became an sdk.notice, which the pane shows as an alert banner. The SDK says the CLI shows 'info' only in transcript mode and 'notice' as inactive gray text (its per-tool progress lines among them), so that chatter interrupted screen readers and replaced more important notices. Only warnings, suggestions and messages that stop the turn are notices now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… pane's own When ExitPlanMode returned the CLI to a mode outside the pane's four (the SDK's 'auto' or 'dontAsk'), the pane kept 'plan', and its next send put the CLI back into plan mode. The pane now drops its explicit mode instead, so the send states none and the CLI keeps its own (logged as fresh_agent.permission_mode_released). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l no pane shows ends with its turn A session error that was not a restore failure wiped the whole live tail, including blocks whose transcript lines were announced but not yet written and the list of lines the pane was waiting for. A failed turn's last answer (its result, then the error, about 100 ms before the CLI writes the line) vanished, sometimes until an unrelated refresh; a refused configure in the middle of a block wiped the block and dropped the rest of its text. The turn's end is now its own signal: a result or an idle status marks the tail's blocks ended and drops the retry; an error drops only the retry. The pane's clear (once nothing is awaited and the pane is not busy) removes the ended blocks and keeps a block whose turn is still streaming. Tails were also never bounded for sessions this client does not show: every client receives every session's stream, and only a mounted, visible pane ever cleared a tail. Visible panes now register as the tail's viewers; at the turn's end a tail no visible pane settles is dropped. A stopped recovery now applies the session's queued stream events before it removes the session, so no later flush re-creates an orphan tail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its line lands A prompt sent right after a turn ended (by hand, or a queued message sent automatically) showed its local echo above the previous answer's live text, which had not landed in a snapshot yet; the text jumped above the prompt only when its line landed, and the next turn's streamed text stacked after the stale answer. The transcript now takes the local echo as its own prop and renders the ended turn's live blocks right above it, with the running turn's blocks and retry below it. Without an echo the tail renders as one piece, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…changed A claude pane now refetches its snapshot after every message the CLI writes, and each read is a fresh parse of the whole transcript: every commit handed the transcript new turn and item objects, and every item card re-rendered and re-parsed its markdown, several times a second during a turn in a long session. Snapshot commits now keep the previous turn object (and, inside a changed turn, each previous item object) whenever the content is the same (withSharedTurns), and the item card and markdown body are memoized. In the pane test, three refetches of a 40-answer transcript went from 240 markdown renders to none, and a refetch adding one answer renders only that answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… plan mode The simulator always switched to 'default' after an approved ExitPlanMode; the pinned CLI returns to the mode it had before plan mode (B6 found bypassPermissions). The only conformance scenario went default -> plan -> exit, so it could not tell them apart. A new plan-return scenario starts in bypassPermissions, pins the real behavior as a contract fact, and holds the simulator to it; the simulator now remembers the mode it left. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…TRY marker The simulator's api_retry step was checked only by its own self-tests. A RETRY:<status>x<n> prompt marker, understood by both the loopback provider (which fails that prompt's request n times, then answers) and the simulator, drives a new retry conformance scenario, with the real behavior pinned as a contract fact (F11). It showed two simulator divergences, now fixed: the CLI replays the prompt only after the retried request is answered, and a retried request does not add to num_turns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # crates/freshell-freshagent/src/claude.rs # crates/freshell-freshagent/src/claude_snapshot.rs # crates/freshell-freshagent/src/snapshot.rs # crates/freshell-ws/tests/common/claude_rig.rs
… streams With the simulator now retrying before it replays the prompt, as the pinned CLI does, the browser lane showed a new session's first request retrying with no Stop button: nothing said the turn was running. The session's transcript does not exist until that request is answered, so the snapshot 404s, and no stream has opened yet. A retry is a request of a running turn, so the client now marks an idle session running when one arrives (a running session is not written again). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The retry mark (and the first stream block's) changed only an 'idle'
session, but a just-created session is 'connected' until its first turn,
so the browser lane still showed a new session's first request retrying
with no Stop button. Both now mark any ready session ('idle' or
'connected') running; other statuses are left alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What users see now
In a freshclaude or kilroy pane, a running turn is visible while it runs instead of only after it ends:
/compactand the CLI's own warnings show in the pane's notice banner without ending the turn.Phase A (the saved-transcript projection: delegation blocks, compaction cards,
/costoutput, the diff panel,[image], API error cards) shipped in #860; this PR is the live part on top of it.What changed, by phase
crates/freshell-claude-sidecar): the SDK message mapping is a pure module (sdk-message-frames.mjs). The sidecar announces each transcript line it expects the CLI to write (sdk.session.changed {reason:'transcript', uuid}), stamps stream events with their API message id, and streams only what the pane renders. API retries becomesdk.retry; rate limits, failed compactions and the CLI's prominent informational messages becomesdk.notice(never a turn end). The CLI's own permission-mode change becomessdk.session.metadata.freshell-freshagent,freshell-ws): retries and notices reach every client (freshAgent.retry,freshAgent.notice) with structured logs; the CLI's permission-mode change folds into the session's settings and is stated in metadata. Snapshots read the transcript store the lane's sessions write, and the projection is incremental (only lines appended since the last read are parsed, off the async runtime).freshAgentLivestore slice holds the live tail (streaming blocks, the retry, notices), so token-rate updates never touch the sessions map. A 100 ms batcher feeds it. The tail renders through the transcript's own turn article and hides each block in the same frame its transcript line lands (by line uuid, or by message id and position when the uuid frame was missed). Panes refetch the snapshot per announced line, re-poll a bounded number of times for lines the CLI is slow to write, and clear the tail once the turn's lines are on screen.test/e2e-browser/helpers/claude-sim-lane.ts) runs the production sidecar over the CLI simulator;freshclaude-live-transcript-rust.spec.tscovers streamed text, tool rows, retries,/compact, delegations, the diff panel,/cost, the plan-mode exit and a reload, for freshclaude and kilroy.docs/index.htmlshows a turn rendering live.Review fixes in this PR (after the phase work)
autoordontAsk) makes the pane drop its own mode, so the next send no longer puts Claude back into plan mode.info,notice, per-tool progress) no longer raise an alert banner; only warnings, suggestions and messages that stop the turn do.connected, notidle) now marks it running too.plan-returnscenario), and API retries are compared against the real CLI through a sharedRETRY:<status>x<n>prompt marker (newretryscenario). That comparison found the CLI replays the prompt only after a retried request is answered, which is what exposed item 7.origin/main(#870, core history) is merged in; the conflicting snapshot helpers now use main'sdisk_snapshot(genesis and rollback aware) with this branch's incremental projection.Tests (all at the final commit unless noted)
pnpm run test:vitest run test/unit/claude-sidecar test/unit/client/components test/unit/client/lib test/unit/client/store test/e2e test/unit/contracts --config config/vitest/vitest.config.ts— 6949 passed, 1 failed:terminal-file-link-same-tab(terminal links, unrelated to this work; the host's load average was about 20), which passed on 3 reruns alone. An earlier full run had a similar one-off inopen-tab-session-sidebar-visibility, also unrelated and also green on 3 reruns.pnpm run test:vitest run test/integration/providers/ --config config/vitest/vitest.config.ts— 89 passed (after the merge).cargo test -p freshell-freshagent --lib— 1496 passed;cargo test -p freshell-ws --test freshagent_claude_live_rendering --test freshagent_claude_real_cli --test freshagent_claude_lifecycle --test freshagent_claude_rollback— 2, 15, 12 and 17 passed (after the merge).GCLOUD_ROBOT_REQUIRE=1 pnpm run test:e2e <freshclaude-live-transcript-rust, fresh-agent-control-rust, fresh-agent-error-turn-attention, freshclaude-durable-transcript-rust, fresh-agent-rollback-rust> --project=chromium— 57 passed, none retried (Cloud Run, image of the final commit). Earlier runs in this PR found the retry case failing (Stop missing during a new session's first-request retries), which fixes 7 address.Follow-ups
fresh-agent-error-turn-attention.spec.tsonto the sharedClaudeSimLanehelper once core PR-D (which edits that spec) lands.workspaceoption are still checked only by the simulator's own tests, not against the real CLI.auto,dontAsk), the gear shows the pane's default mode rather than the CLI's.🤖 Generated with Claude Code