feat: REPL rendering polish — event lines, context cutoff, tool visibility (by Wren) - #401
Conversation
…visibility Four transcript rendering improvements from live heartbeat observation: 1. Labels flush with content: message labels (you, myra, heartbeat) now align with the text below them in the Ink renderer 2. Context cutoff divider: a dim full-width rule marks where the loaded context window begins — printed at live compaction and on reattach when hydration collapsed at a compaction event. Everything above the line is out of the prompt; everything below is what the SB sees. 3. New 'event' message role: compact dim unlabeled lines for progress and status output (tool runs, signals, surfaced memories, budget warnings, dividers) instead of full 'system'-labeled message blocks. printEvent() routes to it in Ink mode, plain dim lines in legacy. 4. Tool visibility: the continuation indicator now names the tools that ran (⋯ ran get_inbox, signal_status — continuing 1/5) and the Ctrl+O context inspector gains a Recent Tool Calls section (last 25, with status and time). The SB's own bookkeeping activities (state_change, tool_call echoes) render as dim event lines instead of loud ⚡ activity blocks — they're progress, not conversation. Co-Authored-By: Wren <noreply@anthropic.com>
conoremclaughlin
left a comment
There was a problem hiding this comment.
Changes requested.
The main UX shape looks good, but the compaction reattach path has a blocker: hydrateLedgerFromTranscript() collapses the ledger at a compaction marker while leaving the visible tailPreview from pre-compaction events intact. On reattach, the new cutoff divider is printed before replaying tailPreview, so old turns that are no longer in the prompt window appear below the “in context” divider, and kept entries can appear duplicated.
Please reset/rebuild tailPreview when a compaction event becomes the new start state (mirroring messageCount = 0 / ledger eviction), and add a regression assertion that tailPreview contains only the compaction kept tail plus post-marker messages.
Verification I ran:
npx vitest run packages/cli/src/commands/chat-hydration.test.ts packages/cli/src/repl/ink/MessageLine.test.ts packages/cli/src/repl/ink/dock-snapshot.test.ts packages/cli/src/repl/ink/dock-resize.test.ts— 48/48 passedyarn workspace @inklabs/cli type-check— passedgit diff --check origin/main...HEAD— clean
Lumen's review catch: the visible replay preview accumulated from pre-compaction events survived the compaction collapse, so on reattach old out-of-context turns appeared BELOW the cutoff divider and kept tail entries duplicated (once from their original pre-marker events, once from the keptEntries re-seed). Clear the preview at the marker (mirroring messageCount = 0); the kept tail re-populates it from the event. Regression test asserts tailPreview is exactly kept tail + post-marker messages, no duplicates. Co-Authored-By: Wren <noreply@anthropic.com>
conoremclaughlin
left a comment
There was a problem hiding this comment.
LGTM.
The prior blocker is fixed: hydration now resets tailPreview at the compaction marker before re-populating it from keptEntries, so the visible replay below the cutoff divider matches the actual loaded window instead of including stale pre-compaction turns. The new regression test asserts the exact kept-tail + post-marker preview and guards against duplicate kept entries.
Verification I ran:
npx vitest run packages/cli/src/commands/chat-hydration.test.ts packages/cli/src/repl/ink/MessageLine.test.ts packages/cli/src/repl/ink/dock-snapshot.test.ts packages/cli/src/repl/ink/dock-resize.test.ts— 49/49 passedyarn workspace @inklabs/cli type-check— passedgit diff --check origin/main...HEAD— clean
## Why Regression from #401's system_turn rendering: heartbeat triggers moved from `type: 'user'` to `type: 'system_turn'`, but the `tailPreview` replay only included user/assistant/inbox roles. On reattach, Conor saw Myra's heartbeat *answers* with no heartbeat *prompts* above them — answers without questions. ## What - `tailPreview` gains a `system` role with a `label` field; `system_turn` events and kept-tail system entries with channel labels (`heartbeat`, `telegram`, …) replay with their label - New `INTERNAL_SYSTEM_SOURCES` set: runtime bookkeeping (continuation prompts, activity echoes, passive recall, budget monitor, bootstrap) stays in the ledger but out of the visible replay - Replay renderer maps system entries to their channel label ## Verification - 9/9 hydration tests (2 new: labeled system turns in preview; visibility preserved through compaction keptEntries with internal sources excluded) - CLI type-check clean 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Why
Conor's observations from watching Myra's live transcript after the PR #400 heartbeat fixes:
you,myra) hang to the left of their content — misalignedstate_change:session_updateechoes render as loud ⚡ activity blocks — bookkeeping noise presented as conversation↳ continuing with tool results (1/5)doesn't say which tools ran, was mislabeled as a "system" message, and Ctrl+O didn't show tool historyWhat
1. Labels flush with content —
MessageLine.tsxlabel row now shares the content's left edge.2. Context cutoff divider — new
renderContextCutoff(): a dim full-width ruleprinted at live compaction, and on reattach when hydration collapsed at a compaction event (new
compactionCollapsedflag threaded throughHistoryHydrationResult). Everything above the line is out of the prompt window.3. New
eventmessage role +printEvent()— compact dim unlabeled lines for progress/status output: tool runs, signals, surfaced memories, budget warnings, dividers, compaction notices. In Ink mode these previously rendered as fullsystem-labeled message blocks (the noise in Conor's screenshot). Legacy mode unchanged (plain dim lines).4. Tool visibility
⋯ ran get_inbox, signal_status — continuing (1/5)…state_change,tool_call/tool_resultechoes) render as dim event lines instead of ⚡ activity blocks; other agents' activities unchangedVerification
⋯ ran get_timezone — continuing (1/5)…renders with tool names; signal + result JSON paths unchangedshared/dist(it restarts the dev server and kills in-flight SB runs)🤖 Generated with Claude Code