Agent chat: long thoughts fold once done, behind a Debug toggle

Merged
TE
22d ago
+491-19

A long reasoning trace takes over the agent pane. With this change, a thought taller than the transcript's view folds to one line once it's done: "Thought", its first line, and a trailing chevron. The full text is one click away. While it streams, the thought still shows in full and the transcript keeps following it.

The default is unchanged. On 2026-10-13 Leo decided that a thought always shows in full (#20330). The fold is therefore behind the Debug toggle agentPane.debug.foldLongThoughts (Debug Settings › Pages › "Fold long thoughts in the agent pane"), which is off by default. With it off, a thought draws exactly as it does today, and tests cover that. Whether it becomes the default is Leo's call.

Catppuccin LatteCatppuccin Mocha
Streaming (in full, followed)
Done, folded
Opened by the reader

These stills come from the gallery variants agent-pane.transcript › long-thought-streaming / long-thought-done, with the toggle's data-fold-long-thoughts set.

Behaviour (toggle on)

  • Threshold: the thought folds when its drawn height is greater than the transcript scroller's clientHeight. There is a 400px floor (20 lines), so a tiny pane doesn't fold every thought. A thought exactly as tall as the view stays open, and one 1px taller folds. This "about one screen" rule comes from the UI coordinator's spec, not from Codex (see below).
  • When it folds: only once the thought is done, meaning the next step or text arrives after it, or the turn ends. turns.ts marks the live turn's latest step row live and gives it its own version, so the row redraws as done when another row follows it.
  • The reader wins: open = choice ?? !folded. The reader's choice survives new content and the virtualized transcript unmounting and remounting the row. Find in Chat opens a folded thought that holds the match.
  • Motion: the automatic fold snaps. The reader's toggle animates the height for 200ms ease-out. Under Reduce Motion nothing animates, and the chevron already snaps (.cv-rotor).
  • Scroll: rows above the thought keep their place, because VirtualTranscript's top-row anchor already does this. A followed transcript stays at its end, using the follow mode from #20697.

Codex reference

Captured from the open-source TUI (openai/codex, codex-rs/tui/src) and Codex desktop footage (ChatGPT 26.930.31730, cmux-app-screenshots/references/codex-tool-collapse-2026-10-14/). Full notes: ~/Projects/cmux-cgc-notes/codex-trace-collapse.md.

  • Codex has no reasoning threshold, because it never shows finished reasoning inline.
    • TUI: a finished reasoning cell is transcript_only (history_cell/messages.rs:750-758, 400-406), so its text appears only in the Ctrl+T transcript overlay.
    • Desktop: no reasoning text appears in any capture. The whole turn's work folds under "Working for Ns" → "Worked for Ns ›".
  • Streaming (desktop and TUI): live work shows in full, and nothing folds mid-stream. The TUI status line is the latest reasoning heading with #/** stripped (chatwidget/streaming.rs). We strip those marks the same way in the folded line.
  • When it folds (desktop): at the moment the next output starts, within one ~280ms capture, not at turn end. We fold when the next step or text arrives.
  • Done label (TUI history_cell/separators.rs, desktop): "Worked for 1m 51s ›". There is no "Thought for Ns" anywhere, and acpmux sends no thought timing, so we show no duration.
  • Motion (desktop): the automatic fold snaps. The reader's open runs top-down over about 300ms; on close, the content goes at once and what follows slides up within about 250ms. Easing and desktop reduced-motion behaviour can't be determined from the stills. The TUI drops its shimmer under MotionMode::Reduced.
  • Scroll (desktop): the header stays put and what follows moves. This is inferred from the stills, not measured.

Borrowed from AI Elements

From shadcn AI Elements' Trace:

  • The open/close rule open ?? running, where the reader's click wins.
  • The trailing chevron that turns 90°.
  • Muted text, with no card, background or badge on the line.
  • One left rule for the opened text: 7px in, 1px --cv-rule, 16px padding.
  • transition-[height] duration-200. It's done here with the Web Animations API so it can be skipped under Reduce Motion and tested.

Validation

  • bun test src/agent-session/acpmux/conversation/ThoughtRow.test.tsx: 9 pass. Covers:
    • toggle off means no fold;
    • viewport boundaries at −1/0/+1px and the 400px floor at −1/0/+1px;
    • no fold while streaming, then a snap fold;
    • an opened thought stays open across a redraw and a remount;
    • a 200ms animation on open and close;
    • Reduce Motion means no animation;
    • preview stripping.
  • CMUX_BROWSER_TESTS=1 bun test test/agent-pane-chat-column.test.ts: 11 pass. Three tests are new:
    • while the reader is scrolled up, the fold leaves "Paragraph 12." at the same top in every painted frame;
    • opening and closing a folded thought keeps its line still;
    • with the toggle off, a long thought stays in full.
  • The transcript/turn suites in test/ and src/agent-session pass, and bun run check (vp check, typecheck, lint, layers) is clean.
  • The first commit (tests only) fails because ThoughtRow has no thoughtLayout; the second commit makes it pass.
  • In-app, on the tagged build cg-composer (reload.sh --tag cg-composer, built under the lane lock; HEAD 92521b907f4): I imported an acpmux session whose turn holds a 60-paragraph thought, opened it in an agent pane, and drove it with cmux app call debug.agent_pane. With agentPane.debug.foldLongThoughts on, the opened turn shows the thought folded to one line (transcript 390px). Clicking the line opens it (7610px, full text) and clicking again folds it back, with the scroll position unchanged. With the toggle off, the same session shows the thought in full and no fold button. The local harnesses here do not stream thoughts, so the streaming → done transition is covered by the unit and browser tests rather than in the app. The tagged build skips the Ghostty CLI helper (CMUX_SKIP_ZIG_BUILD=1, since this machine has zig 0.17 and the helper needs 0.16); the agent pane does not use it.

Localization: no new user-facing strings. The folded line reuses tools.thought. The Debug Settings label is DEV/NIGHTLY-only English, like the other tunables.

🤖 Generated with Claude Code