Skip to content

feat(tui): simplify conversation detail and quiet chat output - #2193

Open
kevinjosethomas wants to merge 24 commits into
mainfrom
feat/ui-chat-output
Open

feat(tui): simplify conversation detail and quiet chat output#2193
kevinjosethomas wants to merge 24 commits into
mainfrom
feat/ui-chat-output

Conversation

@kevinjosethomas

@kevinjosethomas kevinjosethomas commented Sep 10, 2026

Copy link
Copy Markdown
Member

Conversations open with assistant text visible, thinking hidden, tools collapsed, and compact per-file change summaries. The configurable Ctrl+O shortcut cycles through overview, thinking plus file diffs, and all output. The selected mode applies to streaming content and restored chats. Thinking renders as dim text without a repeated heading; expanded file diffs start at the normal chat inset while preserving code indentation and line-number gutters. A single status shows “Showing overview”, “Showing details”, or “Showing all output” with the configured expand or collapse shortcut. Unbound shortcuts omit the hint. Transcript rows, including sent and received agent messages, no longer repeat expansion hints.

Background shell completions update an identifiable originating IPython row: a running marker becomes a success check or a failure with its exit code. Matching prefers an exact saved BashHandle PID and command. Assignment-only cells do not save a handle, so a successful simple literal assignment can match by its unique exact full command; repeated commands, conflicting known PIDs, complex launches, and unmatched events retain compact “Background shell command finished” or failure notices. Completion rows share tool-row spacing without an extra gap below. All output preserves the complete notification beneath the command and a timestamped marker at its original transcript position. Cell duration remains explicitly labeled as cell timing because completion events do not contain shell duration or output.

Assistant prose, thinking, collapsed tool previews, and trailing event details use quieter colors and less decorative bold. Sent and received agent messages keep compact notices in overview and details, with their full message bodies shown only in all output. Their separate Ctrl+P action and shortcut are removed. Multiline Python strings preserve their syntax color across source lines and narrow wrapping, including embedded Markdown and code snippets. Collapsed Python previews select statements outside string bodies. These are presentation changes; saved messages, JSONL traces, model/tool execution, and transport behavior are unchanged.

Validation: 34 focused Python preview and rendering tests passed for multiline strings, raw strings, streamed incomplete strings, resumed token colors, and narrow wrapping, plus a read-only reproduction of the reported saved cell. 206 tests across the three affected rendering, status, and live/replay files passed for the compact-notice behavior, plus npm run check. The broader shortcut and shell regression checks also passed. Faux-provider regressions cover matched and unmatched shell success/failure, assignment-only launches, completion-before-tool-result ordering, duplicate notifications, escaped commands, compact completion adjacency, live/restored agent-message visibility, and unchanged saved trace bytes.

Design context: UI proposals.


Note

Medium Risk
Large user-facing TUI and keybinding changes with new shell-completion matching logic, but presentation and settings defaults only—saved session traces and tool execution are unchanged.

Overview
Unifies conversation expansion behind a single Ctrl+O three-step cycle—overview (thinking hidden, tools/diffs/agent bodies compact) → details (thinking + file diffs visible, tool output still collapsed) → all output—and removes the old Ctrl+J, Ctrl+T, and Ctrl+P bindings. Detail mode is presentation-only (tray status shows the current step and shortcut; transcript rows no longer repeat expand hints). Thinking defaults to hidden in overview and renders as dim trace text without a header when revealed.

Visual hierarchy adds theme token mdBody for assistant prose, dims collapsed tool/IPython/bash previews and trailing row metadata, trims decorative bold, and aligns expanded file diffs with the normal chat inset.

Background shell completions can attach to matching tool/IPython rows (PID + command, with a narrow fallback for unique assignment-only launches); otherwise compact fallback notices remain, with full notifications in all output. Python previews ignore multiline string bodies; expanded cells keep syntax coloring across wrapped lines.

Settings drops the separate “hide thinking” toggle (behavior follows the detail cycle). Theme schemas require five new color tokens (mdBody, diff backgrounds/text).

Reviewed by Cursor Bugbot for commit efa801a. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Simplify TUI conversation detail cycle and quiet chat output styling

  • Replaces separate tool, agent-message, edit-diff, and thinking expansion settings with a single 3-state cycle (overview, details, all-output) triggered by Ctrl+O.
  • Removes the hideThinkingBlock setting and its callback. Thinking blocks now start hidden and their visibility is controlled by the new detail cycle.
  • Removes bold styling from tool titles, panel headings, and metadata. Adds a required mdBody theme color token for assistant markdown text and applies dim styling to quiet output.
  • Adds dedicated ShellCompletionComponent for asynchronous Bash completion messages, attaching them to the matching preceding tool execution.
  • Risk: Removes message-expansion, edit-expansion, and thinking-toggle keybindings and their legacy migrations in keybindings.ts; existing configs referencing these may need manual updates.

Macroscope summarized efa801a.

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown

Prime Agent performance — completed

PR efa801a5 compared with main 1eee2938.

Overall: 0 regressed · 0 improved · 17 no clear change.

Metric Main This PR Change
Cold startup 2,934.0 ms 2,940.2 ms ≈ +6.2 ms (+0.21%)
Warm startup 1,748.4 ms 1,776.7 ms ≈ +28.2 ms (+1.62%)
Installation 26.80 s 26.86 s ≈ +0.07 s (+0.25%)
Compressed release artifacts 11.12 MB 11.10 MB ≈ -0.02 MB (-0.16%)
Installed footprint 597.49 MB 597.43 MB ≈ -0.06 MB (-0.01%)
Idle memory, summed RSS 1,036.85 MB 1,041.00 MB ≈ +4.16 MB (+0.40%)

Python runtime

Metric Main This PR Change
Python kernel startup 136.3 ms 149.3 ms ≈ +13.0 ms (+9.51%)
Python cell round trip 0.566 ms 0.566 ms ≈ +0.00034 ms (+0.06%)
Empty bash command 11.7 ms 12.5 ms ≈ +0.8 ms (+6.80%)
Bash git status 17.9 ms 18.4 ms ≈ +0.5 ms (+2.78%)
Bash 32 KiB output 12.7 ms 12.8 ms ≈ +0.044 ms (+0.34%)
35 cells / 9 shell calls 180.0 ms 188.8 ms ≈ +8.8 ms (+4.90%)
Python interrupt to done 1.518 ms 1.634 ms ≈ +0.116 ms (+7.65%)
Python state snapshot 26.9 ms 27.3 ms ≈ +0.4 ms (+1.53%)
Python state restore 380.5 ms 377.0 ms ≈ -3.5 ms (-0.91%)
Python idle RSS 34.06 MB 36.32 MB ≈ +2.26 MB (+6.63%)
Python RSS after pandas workload 96.50 MB 98.33 MB ≈ +1.83 MB (+1.90%)

Sandbox cost: ~$0.0880 — no inference calls.
Run, logs, and downloadable raw results

Methodology and samples

Main resolved at 2026-09-10T23:57:28.483618+00:00. Harness 1eee2938.
Linux x64, 4 vCPU, 8 GB RAM, 20 GB disk; region us.
Image: node:24-bookworm@sha256:be23f54a88d34e8824c741b19b91064094f92c1c97b194144bfc8b50d67258e2.
Stock tools, skills, daemon, and Python bootstrap enabled; fresh homes and a fixed Git fixture.
Onboarding is dismissed; the editor starts without a selected model or submitted prompt.
Medians shown. Arrows require a 20% timing/memory change plus absolute floors and IQR.
These practical noise floors are not a statistical significance test.
Cold means stopped Prime processes; OS filesystem caches are not flushed.
No model requests or credentials. Installation excludes build/setup time.
Installer tarballs use loopback; npm/Python downloads use the network with fresh caches.
Artifact size counts release tarballs; footprint after first use includes registry packages.
MB is decimal. Summed RSS can double-count shared pages; PSS is recorded when available.
Provisioning, setup, and build durations are recorded separately in the raw results.
Kernel probes use the installed JSONL runtime, outside the TUI/TypeScript host.
Per trial: 50 Python cells, 5 calls per shell case, and one 35-cell mix (9 git status calls).
Cell/shell values are batch means; other runtime timings are single operations.
State fixture: a 10,000-row × 8-column integer DataFrame and a 10,000-integer list.
Restore runs in a fresh kernel, including pandas imports; kernel startup is excluded.
Kernel RSS covers the isolated Python process; loaded RSS follows the pandas workload.
Costs estimate full sandbox lifetimes at configured rates, including setup and build.
Budget target: $1; not a billing cap. Checks are informational.

Metric Main successful/attempted PR successful/attempted Main spread PR spread
Cold startup 10/10 10/10 IQR 200.2 ms IQR 282.2 ms
Warm startup 10/10 10/10 IQR 138.8 ms IQR 206.2 ms
Installation 3/3 3/3 range 1.74 s range 1.64 s
Compressed release artifacts 1/1 1/1
Installed footprint 1/1 1/1
Idle memory, summed RSS 10/10 10/10 IQR 96.05 MB IQR 102.83 MB
Python kernel startup 10/10 10/10 IQR 17.0 ms IQR 30.0 ms
Python cell round trip 10/10 10/10 IQR 0.069 ms IQR 0.133 ms
Empty bash command 10/10 10/10 IQR 1.7 ms IQR 3.5 ms
Bash git status 10/10 10/10 IQR 2.8 ms IQR 2.3 ms
Bash 32 KiB output 10/10 10/10 IQR 1.4 ms IQR 1.6 ms
35 cells / 9 shell calls 10/10 10/10 IQR 18.0 ms IQR 20.7 ms
Python interrupt to done 10/10 10/10 IQR 0.154 ms IQR 0.300 ms
Python state snapshot 10/10 10/10 IQR 2.5 ms IQR 3.0 ms
Python state restore 10/10 10/10 IQR 47.5 ms IQR 40.8 ms
Python idle RSS 10/10 10/10 IQR 3.44 MB IQR 0.86 MB
Python RSS after pandas workload 10/10 10/10 IQR 5.18 MB IQR 1.62 MB

"toolPanelBg",
"toolTitle",
"toolOutput",
"mdBody",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High theme/theme-schema.json:63

Existing custom themes without mdBody are accepted during the initial load, but rendering an assistant paragraph then calls Theme.fg("mdBody", ...) and throws Unknown theme color: mdBody, breaking the TUI. Make mdBody optional with a fallback to text, or migrate legacy themes before rendering.

Also found in 1 other location(s)

packages/coding-agent/src/modes/interactive/components/assistant-message.ts:301

This unconditionally looks up mdBody, but existing user theme JSON files do not have that newly required key. On the initial custom-theme load, parseThemeJson deliberately performs only its minimal structural check before the async validator is ready, so such a theme is accepted and constructed without an mdBody foreground. Rendering any ordinary assistant paragraph then calls Theme.fg("mdBody", ...), which throws Unknown theme color: mdBody and breaks the TUI render. Provide a fallback (for example the previous terminal/default text color) or migrate/default legacy custom themes before using the new token.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/interactive/theme/theme-schema.json around line 63:

Existing custom themes without `mdBody` are accepted during the initial load, but rendering an assistant paragraph then calls `Theme.fg("mdBody", ...)` and throws `Unknown theme color: mdBody`, breaking the TUI. Make `mdBody` optional with a fallback to `text`, or migrate legacy themes before rendering.

Evidence trail:
Reviewed commit 7412daf. packages/coding-agent/src/modes/interactive/theme/theme-schema.json:37-94 requires mdBody; packages/coding-agent/src/modes/interactive/theme/theme.ts:63-68 adds mdBody as required in the runtime schema; theme.ts:672-725 shows the initial minimal validation and later non-mutating validation; theme.ts:760-786 constructs colors only from supplied entries; theme.ts:400-403 throws for missing colors; packages/coding-agent/src/modes/interactive/components/assistant-message.ts:295-302 calls theme.fg("mdBody", ...). Commands: git_diff --base MERGE_BASE --head REVIEWED_COMMIT -- packages/coding-agent/src/modes/interactive/theme/theme-schema.json; git_grep mdBody|Unknown theme color|Theme.fg.

Also found in 1 other location(s):
- packages/coding-agent/src/modes/interactive/components/assistant-message.ts:301 -- This unconditionally looks up `mdBody`, but existing user theme JSON files do not have that newly required key. On the initial custom-theme load, `parseThemeJson` deliberately performs only its minimal structural check before the async validator is ready, so such a theme is accepted and constructed without an `mdBody` foreground. Rendering any ordinary assistant paragraph then calls `Theme.fg("mdBody", ...)`, which throws `Unknown theme color: mdBody` and breaks the TUI render. Provide a fallback (for example the previous terminal/default text color) or migrate/default legacy custom themes before using the new token.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/coding-agent/src/modes/interactive/components/assistant-message.ts Outdated
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts Outdated
@kevinjosethomas kevinjosethomas changed the title fix(tui): soften assistant output text color feat(tui): simplify conversation detail and quiet chat output Sep 10, 2026
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium

this.defaultEditor.setPaddingX(editorPaddingX);

Persisted hideThinkingBlock: false settings are ignored, so existing users start with thinking blocks hidden until they cycle the chat-detail control. Removing the getHideThinkingBlock() assignment leaves the field's default true value in place; restore the assignment during runtime settings application.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/interactive/interactive-mode.ts around line 2563:

Persisted `hideThinkingBlock: false` settings are ignored, so existing users start with thinking blocks hidden until they cycle the chat-detail control. Removing the `getHideThinkingBlock()` assignment leaves the field's default `true` value in place; restore the assignment during runtime settings application.

Evidence trail:
packages/coding-agent/src/modes/interactive/interactive-mode.ts:1043, 1511-1514, 2554-2564, 2846-2856, 6461-6476, 7322-7341, 7353-7366; packages/coding-agent/src/core/settings-manager.ts:961-969. Reviewed commit short hash: 4d3fcdf. Verify with: git diff MERGE_BASE REVIEWED_COMMIT -- packages/coding-agent/src/modes/interactive/interactive-mode.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

private editDiffsExpanded = false;

private hideThinkingBlock = false;
private hideThinkingBlock = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Orphaned hideThinkingBlock settings API

Medium Severity

hideThinkingBlock is now a presentation flag on the Ctrl+O cycle, but SettingsManager.getHideThinkingBlock / setHideThinkingBlock and the settings field still exist with no non-test callers. The TUI never reads the setting, so hideThinkingBlock: false is silently ignored while docs/settings.md still documents it as a working option.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by project rule: Review rules

Reviewed by Cursor Bugbot for commit 4d3fcdf. Configure here.

Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts
this.dirty = true;
}

/** True when the message carries a non-empty thinking block. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unused thinking-content helper remains

Low Severity

hasThinkingContent() is still a public method on AssistantMessageComponent, but this change deleted its only caller (toggleThinkingBlockVisibility). It now has no non-test consumers.

Fix in Cursor Fix in Web

Triggered by project rule: Review rules

Reviewed by Cursor Bugbot for commit 4d3fcdf. Configure here.

.split("\n")
.filter((line) => !/^(?:from rlm import bash|import rlm)\s*$/.test(line));
// Restrict to a single top-level launch, optionally followed by its variable.
const call = /^(?:([A-Za-z_]\w*)\s*=\s*)?(?:rlm\.)?bash\(\s*(.*)\s*\)\s*$/.exec(launch[0] ?? "");
const launch = code
.trim()
.split("\n")
.filter((line) => !/^(?:from rlm import bash|import rlm)\s*$/.test(line));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium components/shell-completion.ts:74

A blank line after the rlm import makes readBackgroundShellHandle reject an otherwise valid single launch, so the completion remains detached for cells such as from rlm import bash\n\njob = bash('npm test')\njob. The normalization removes import lines but retains empty lines, causing launch[0] to miss the bash(...) call; filter blank lines as well.

Suggested change
.filter((line) => !/^(?:from rlm import bash|import rlm)\s*$/.test(line));
.filter((line) => line.trim() !== "" && !/^(?:from rlm import bash|import rlm)\s*$/.test(line));
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/interactive/components/shell-completion.ts around line 74:

A blank line after the `rlm` import makes `readBackgroundShellHandle` reject an otherwise valid single launch, so the completion remains detached for cells such as `from rlm import bash\n\njob = bash('npm test')\njob`. The normalization removes import lines but retains empty lines, causing `launch[0]` to miss the `bash(...)` call; filter blank lines as well.

Evidence trail:
Commit 783d8d4: packages/coding-agent/src/modes/interactive/components/shell-completion.ts:71-78; packages/coding-agent/src/modes/interactive/components/tool-execution.ts:268-271; packages/coding-agent/src/modes/interactive/components/conversation-components.ts:85-95.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 5 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 783d8d4. Configure here.

Text,
truncateToWidth,
visibleWidth,
} from "@earendil-works/pi-tui";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dead thinking-label API remains

Low Severity

Removing the thinking heading left hiddenThinkingLabel and setHiddenThinkingLabel with no effect on output. The constructor argument, option, and signature key still exist and only force rebuilds that render the same dim trace.

Additional Locations (2)
Fix in Cursor Fix in Web

Triggered by project rule: Review rules

Reviewed by Cursor Bugbot for commit 783d8d4. Configure here.

const hint = detailHint ? `${theme.fg("dim", " · ")}${detailHint}` : "";
// Keep the summary path stable across conversation detail modes.
const expandedHint = diffsExpanded === undefined ? "" : expandCollapseHint("app.tools.expand", true);
const widestHint = expandedHint ? `${theme.fg("dim", " · ")}${expandedHint}` : "";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dead file-summary hint logic

Low Severity

formatFileChangeSummaryLine still branches on diffsExpanded and builds hint/widest-hint strings, but expandCollapseHint("app.tools.expand") is always empty, so those strings and the extra width reservation never change the line.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by project rule: Review rules

Reviewed by Cursor Bugbot for commit 783d8d4. Configure here.

);
this.addBlank(lines, width);
this.addPlain(lines, truncateToWidth(agentMessageSummaryLine(label, recipient), Math.max(1, width - 1), "…"));
lines.push(...agentMessageBodyLines(message.message, width));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High components/ipython-cell.ts:675

Large sent agent messages make agentMessageBodyLines return enough entries that lines.push(...lines) exceeds JavaScript's argument limit and throws during TUI rendering. Append each body line iteratively instead, as renderFileDiff does for large diffs.

Suggested change
lines.push(...agentMessageBodyLines(message.message, width));
for (const line of agentMessageBodyLines(message.message, width)) {
lines.push(line);
}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/interactive/components/ipython-cell.ts around line 675:

Large sent agent messages make `agentMessageBodyLines` return enough entries that `lines.push(...lines)` exceeds JavaScript's argument limit and throws during TUI rendering. Append each body line iteratively instead, as `renderFileDiff` does for large diffs.

Evidence trail:
Reviewed commit 9723a966e5e2e118f9363a1d29d6700389a09021: packages/coding-agent/src/modes/interactive/components/ipython-cell.ts:668-676; packages/coding-agent/src/modes/interactive/components/ipython-cell.ts:697-734; packages/coding-agent/src/modes/interactive/components/agent-message.ts:22-32; packages/coding-agent/src/core/kernel/shared.ts:181-207. External semantics: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax

Keep string bodies out of collapsed previews and restore ANSI continuation state before source-line gutters. fixes #2193
kevinjosethomas added a commit that referenced this pull request Sep 11, 2026
kevinjosethomas added a commit that referenced this pull request Sep 11, 2026
kevinjosethomas added a commit that referenced this pull request Sep 11, 2026
Keep string bodies out of collapsed previews and restore ANSI continuation state before source-line gutters. fixes #2193
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.

2 participants