Skip to content

fix(cli): render tool results when cards settle, not at turn end - #3526

Open
Astro-Han wants to merge 2 commits into
mainfrom
fix/3521-live-tool-result-content
Open

fix(cli): render tool results when cards settle, not at turn end#3526
Astro-Han wants to merge 2 commits into
mainfrom
fix/3521-live-tool-result-content

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #3521 — TUI tool cards settled into (no output) and, once scrolled out of the viewport, stayed that way forever; Ctrl+O/Ctrl+T could not touch them.

Root cause: the live tool_start / tool_result broadcast frames carried no args and no result content. A card therefore settled into the empty placeholder, and the only content path was the turn-terminal durable backfill — which arrives after terminal scrollback has already frozen the card's lines (#1139), and the viewport-anchored toggles (#1130) then lock the stale render in place.

The fix makes live events self-sufficient, which is the design the immutable-scrollback medium requires: the card must be renderable at settle time, before it can scroll away. The runtime already holds the full result when it emits the event (persist-then-broadcast in tool-runtime.ts), so the Host now projects it into the live frame instead of dropping it:

  • tool_start frames carry args (bounded to 16 KB serialized; oversized args are omitted, matching today's mid-turn placeholder for that rare case).
  • tool_result frames carry the settled content (bounded to 48 KB serialized, keeping the whole frame under the 64 KB subscription frame cap); an oversized result degrades to its serialized byte size (contentBytes) so the row stays truthful instead of claiming no output.
  • The CLI adapter projector deletes the { kind: 'text', text: '' } substitution and passes both fields through; the TUI settles cards with the real result, so the freeze now preserves a correct render. Oversized results render as N bytes with a dim note in the expanded card, and the existing turn-terminal reconcile (kept as the backstop for shed/oversize edges) heals the full body at turn end.

What deliberately did NOT change: raw provider error text and thinking signatures remain rejected on the wire; the desktop merge paths are untouched (live content simply reaches its projection earlier); the viewport-anchored expansion toggles stay as designed — with content correct at settle time, the "earlier cards show nothing" half of the issue disappears, and the remaining "cannot retoggle scrollback" half is the documented #1097/#1130 tradeoff.

Verification

Same scripted driver, same four mid-turn cards, captured through runMakaPiTui + FakeTerminal.

Before (main) — cards settle to (no output) / duration-only, with no input target:

image

After (this PR) — settle-time facts: invocation target + outcome, including a truthful oversized-result size instead of a no output lie:

image
  • New behavior tests, each seen red before the implementation: live tool_result broadcasts content, tool_start broadcasts args, oversized args/content degrade to omission/contentBytes (frame re-decodes under the subscription cap), the adapter projector passes them through, and the TUI renders 100000 bytes rather than no output until the terminal reconcile heals the body.
  • The privacy-normalization protocol test was rewritten: args/content are now accepted (bounded), while raw provider error text and thinking signature frames remain rejected.
  • @maka/runtime-host workspace suite: 1075/1075 pass. @maka/cli: 374/374. @maka/ui: 201/201. tsc builds and biome format:check are clean.
  • Not run: full-repository suite and desktop E2E (left to CI); no live TUI session was driven end-to-end.

Security

This relaxes a deliberate boundary: live subscription frames were privacy-normalized to exclude tool args/results. The same data was always available to the same clients through the durable transcript read path, and tool_output_delta already streams raw output live; this change removes the asymmetry. Per the maintainer's call on #3521, that normalization is not a priority for this channel. Bounded sizes keep frames under the existing cap.

AI use

  • Generative tooling made a substantive contribution

Tool(s) and scope: Maka (Kimi K3) diagnosed the root cause, designed the wire shape, and wrote the implementation and tests. Commits carry Generated-by trailers.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above

@Astro-Han
Astro-Han force-pushed the fix/3521-live-tool-result-content branch from c35b556 to a789d4f Compare August 22, 2026 17:35
@Astro-Han
Astro-Han marked this pull request as ready for review August 22, 2026 17:54
Live tool_start/tool_result broadcast frames carried no args or result
content, so a transcript card could only settle into a '(no output)'
placeholder and wait for the turn-terminal durable backfill. Terminal
scrollback cannot be rewritten, so any card scrolled out of the viewport
before the backfill kept the placeholder forever (#3521).

The runtime already holds the full result when it emits the event
(persist-then-broadcast). Project it into the live frame, bounded so the
frame stays under the subscription frame cap: args up to 16 KB, result
content up to 48 KB; an oversized result degrades to its serialized byte
size (contentBytes) so the row stays truthful. The wire keeps rejecting
raw provider error text and thinking signatures.

Generated-by: Maka (Kimi K3)
With content now riding live tool_result frames, a result that exceeds
the 48 KB live budget arrives as an empty placeholder plus contentBytes.
Render the truthful byte count in the compact row (instead of the
'(no output)' lie) and a dim note in the expanded card; the terminal
reconcile still heals the full body from the durable transcript at turn
end, clearing the placeholder.

Generated-by: Maka (Kimi K3)
@Astro-Han
Astro-Han force-pushed the fix/3521-live-tool-result-content branch from a789d4f to 7aa64d2 Compare August 22, 2026 18:33
jackwener
jackwener previously approved these changes Aug 22, 2026

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review of 7aa64d298. Did not treat Astro-Han authorship as evidence.

  1. Problem. Live tool_start / tool_result frames had no args or result. Cards settled to (no output) and terminal scrollback froze that lie before the durable backfill.

  2. Solution. Persist-then-broadcast already has the result; project it onto the live frame. Args ≤16 KB, content ≤48 KB (frame cap 64 KB−1). Oversized args omitted; oversized content becomes contentBytes so the row shows N bytes instead of no output. Durable reconcile still replaces the placeholder at turn end. Error text and thinking signatures stay rejected.

  3. Timing. Events are keyed by toolUseId; later tools do not overwrite earlier cards. content and contentBytes are mutually exclusive on the wire. Reconcile clears resultBytes when real text lands.

Privacy: live frames now carry the same args/results the durable transcript already exposed; tool_output_delta already streamed output. Size bounds keep the existing cap.

Approve. CI test was still running on this head; merge stays with the author.

@jackwener
jackwener dismissed their stale review August 22, 2026 18:50

Third independent line found P1: decodeLiveToolResultContent() only checks kind then casts. Malformed or unknown kinds can pass the wire. Dismissing this APPROVE; head 7aa64d2 is not merge-ready.

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.

bug(cli): TUI tool cards settle to "(no output)" and stay stuck once scrolled out of the viewport

2 participants