diff --git a/packages/coding-agent/.changes/ui-chat-output.md b/packages/coding-agent/.changes/ui-chat-output.md new file mode 100644 index 0000000000..3bf233be32 --- /dev/null +++ b/packages/coding-agent/.changes/ui-chat-output.md @@ -0,0 +1,13 @@ +- Changed assistant message body text to a new dimmed `mdBody` theme color, easing the wall of bright default-foreground text while headings, links, and code keep their styling. +- Changed prime-theme inline code to a darker neutral (#c8c8cd) so it stays distinct from the dimmed body text. +- Added a three-stage Ctrl+O cycle for overview, thinking and file diffs, and all output, replacing the separate Ctrl+J and Ctrl+T conversation shortcuts without changing saved traces. +- Changed thinking rows to stay hidden in overview and appear as dim text without a repeated heading in the other detail modes, including newly streamed thinking. +- Changed collapsed tool-call previews to render plain and dim instead of green or syntax-highlighted code, with dim line counts and durations, while expanded blocks keep full highlighting. +- Changed decorative bold text in the conversation surface to normal weight, keeping bold only where it marks state or a single critical item (selected rows, active tabs, the login verification code). +- Changed the conversation row hierarchy: event-row trailing detail (agent-message participants, tool command previews, line counts, durations) renders dim while leading labels keep their colors. +- Changed background shell completions to update identifiable command rows, with compact fallback notices and full notifications retained in all output. +- Replaced repeated conversation detail shortcut hints with a status label showing the current detail mode and configurable expand or collapse shortcut. +- Changed expanded file diffs to start at the normal chat inset while preserving code indentation and diff gutters. +- Changed sent and received agent messages to keep compact notices in overview and details, show full bodies only in all output, and use the shared detail cycle instead of a separate Ctrl+P toggle. +- Fixed spacing after background shell completions and matched unique literal assignment-only shell launches to their completion notices. +- Fixed multiline Python string colors across source lines and narrow wrapping, and kept embedded string content out of collapsed code previews. diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 5f6bd39454..2fcac6b790 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -182,9 +182,8 @@ See `/hotkeys` for the full list. Customize via `~/.prime/agent/keybindings.json | Escape | Clear the input without interrupting active work | | Escape twice | Open `/tree` | | Ctrl+L | Open model selector | -| Ctrl+P / Shift+Ctrl+P | Cycle scoped models forward/backward | -| Ctrl+O | Collapse/expand tool output | -| Ctrl+T | Collapse/expand thinking blocks | +| Ctrl+P | Collapse/expand agent-to-agent messages | +| Ctrl+O | Cycle overview → thinking and file diffs → all output | ### Message Queue diff --git a/packages/coding-agent/docs/keybindings.md b/packages/coding-agent/docs/keybindings.md index 94248e10da..ccde4aeba6 100644 --- a/packages/coding-agent/docs/keybindings.md +++ b/packages/coding-agent/docs/keybindings.md @@ -115,7 +115,6 @@ Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `ctrl+1 |--------|---------|-------------| | `app.model.select` | `ctrl+l` | Open model selector | | `app.model.toggleScope` | `alt+s` | Toggle between all and scoped models | -| `app.thinking.toggle` | `ctrl+t` | Collapse or expand thinking blocks | ### Configuration Menu @@ -129,15 +128,15 @@ Use `tab` to cycle forward and `shift+tab` to cycle backward through Providers, | Keybinding id | Default | Description | |--------|---------|-------------| -| `app.tools.expand` | `ctrl+o` | Collapse or expand tool output | -| `app.messages.expand` | `ctrl+p` | Collapse or expand agent-to-agent messages | -| `app.edits.expand` | `ctrl+j` | Collapse or expand edit diffs | +| `app.tools.expand` | `ctrl+o` | Cycle overview → thinking and file diffs → all output | | `app.message.followUp` | `alt+enter` | Queue follow-up message | | `app.message.navigateOlder` | `alt+up` | Select the next older pending message | | `app.message.navigateNewer` | `alt+down` | Select the next newer pending message or restore the draft | | `app.message.moveEarlier` | `ctrl+alt+up` | Move the selected pending message one place earlier in its queue | | `app.message.moveLater` | `ctrl+alt+down` | Move the selected pending message one place later in its queue | +Ctrl+O changes presentation only: the default hides thinking and collapses tools and diffs; the first press reveals thinking and file diffs; the second expands tool output and full agent-to-agent message bodies; the third returns to the default. Compact sent and received message notices remain visible in every mode. This also works for restored conversations and new streaming content. Ctrl+J, Ctrl+T, and Ctrl+P no longer control conversation expansion. + ### Tree Navigation | Keybinding id | Default | Description | @@ -185,7 +184,7 @@ On native Windows, `app.suspend` has no default binding because Windows terminal ### Emacs Example -Binding `ctrl+p` below releases the conflicting `app.messages.expand` default; shortcuts in other views keep their defaults. +Binding `ctrl+p` below moves the editor cursor up; shortcuts in other views keep their defaults. ```json { diff --git a/packages/coding-agent/docs/themes.md b/packages/coding-agent/docs/themes.md index 44b8f108e5..740d4c339d 100644 --- a/packages/coding-agent/docs/themes.md +++ b/packages/coding-agent/docs/themes.md @@ -79,9 +79,12 @@ vim ~/.prime/agent/themes/my-theme.json "toolPendingBg": "#1e1e2e", "toolSuccessBg": "#1e2e1e", "toolErrorBg": "#2e1e1e", + "toolDiffAddedBg": "#0a2f0a", + "toolDiffRemovedBg": "#2f0a0a", "toolPanelBg": "#2d2d38", "toolTitle": "primary", "toolOutput": "", + "mdBody": "", "mdHeading": "#ffaa00", "mdLink": "primary", "mdLinkUrl": "secondary", @@ -94,6 +97,7 @@ vim ~/.prime/agent/themes/my-theme.json "mdListBullet": "#00ffff", "toolDiffAdded": "#00ff00", "toolDiffRemoved": "#ff0000", + "toolDiffText": "secondary", "toolDiffContext": "secondary", "syntaxComment": "secondary", "syntaxKeyword": "primary", @@ -140,13 +144,13 @@ vim ~/.prime/agent/themes/my-theme.json - `name` is required and must be unique. - `vars` is optional. Define reusable colors here, then reference them in `colors`. -- `colors` must define all 51 required tokens. +- `colors` must define all 56 required tokens. The `$schema` field enables editor auto-completion and validation. ## Color Tokens -Every theme must define all 51 color tokens. There are no optional colors. +Every theme must define all 56 color tokens. There are no optional colors. ### Core UI (11 colors) @@ -181,10 +185,11 @@ Every theme must define all 51 color tokens. There are no optional colors. | `toolTitle` | Tool title | | `toolOutput` | Tool output text | -### Markdown (10 colors) +### Markdown (11 colors) | Token | Purpose | |-------|---------| +| `mdBody` | Assistant output body text | | `mdHeading` | Headings | | `mdLink` | Link text | | `mdLinkUrl` | Link URL | diff --git a/packages/coding-agent/docs/tui.md b/packages/coding-agent/docs/tui.md index 5e9c371255..4e2d2a48e7 100644 --- a/packages/coding-agent/docs/tui.md +++ b/packages/coding-agent/docs/tui.md @@ -413,7 +413,7 @@ renderResult(result, options, theme, context) { | Messages | `userMessageText`, `customMessageText`, `customMessageLabel` | | Tools | `toolTitle`, `toolOutput` | | Diffs | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` | -| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` | +| Markdown | `mdBody`, `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` | | Syntax | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` | | Thinking | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh` | | Modes | `bashMode` | diff --git a/packages/coding-agent/examples/extensions/hidden-thinking-label.ts b/packages/coding-agent/examples/extensions/hidden-thinking-label.ts index bcdaab66aa..3f2bcd9dc0 100644 --- a/packages/coding-agent/examples/extensions/hidden-thinking-label.ts +++ b/packages/coding-agent/examples/extensions/hidden-thinking-label.ts @@ -2,16 +2,16 @@ * Hidden Thinking Label Extension * * Demonstrates `ctx.ui.setHiddenThinkingLabel()` for customizing the label shown - * when thinking blocks are hidden. + * above visible thinking blocks. * * Usage: * pi --extension examples/extensions/hidden-thinking-label.ts * * Test: * 1. Load this extension - * 2. Hide thinking blocks with Ctrl+T + * 2. Press Ctrl+O once to show thinking and file diffs * 3. Ask for something that produces reasoning output - * 4. The collapsed thinking block label will show the custom text + * 4. The thinking block label will show the custom text * * Commands: * /thinking-label Set a custom hidden thinking label diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index 172f725003..e27b5c9111 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -67,9 +67,6 @@ const RESERVED_KEYBINDINGS_FOR_EXTENSION_CONFLICTS = [ "app.suspend", "app.model.select", "app.tools.expand", - "app.messages.expand", - "app.edits.expand", - "app.thinking.toggle", "app.subagents.focus", "app.editor.external", "app.message.followUp", diff --git a/packages/coding-agent/src/core/keybindings.ts b/packages/coding-agent/src/core/keybindings.ts index 1efbaaca00..6b6c8ad1b1 100644 --- a/packages/coding-agent/src/core/keybindings.ts +++ b/packages/coding-agent/src/core/keybindings.ts @@ -21,9 +21,6 @@ export interface AppKeybindings { "app.model.toggleScope": true; "app.configuration.previousTab": true; "app.tools.expand": true; - "app.messages.expand": true; - "app.edits.expand": true; - "app.thinking.toggle": true; "app.subagents.focus": true; "app.heartbeats.open": true; "app.heartbeats.openSelected": true; @@ -88,18 +85,7 @@ export const KEYBINDINGS = { "app.model.select": { defaultKeys: "ctrl+l", description: "Open model selector" }, "app.model.toggleScope": { defaultKeys: "alt+s", description: "Toggle model selector scope" }, "app.configuration.previousTab": { defaultKeys: "shift+tab", description: "Select previous configuration tab" }, - "app.tools.expand": { defaultKeys: "ctrl+o", description: "Toggle tool output", defaultKeyScope: "editor" }, - "app.messages.expand": { - defaultKeys: "ctrl+p", - description: "Toggle agent message expansion", - defaultKeyScope: "editor", - }, - "app.edits.expand": { defaultKeys: "ctrl+j", description: "Toggle edit diffs", defaultKeyScope: "editor" }, - "app.thinking.toggle": { - defaultKeys: "ctrl+t", - description: "Toggle thinking blocks", - defaultKeyScope: "editor", - }, + "app.tools.expand": { defaultKeys: "ctrl+o", description: "Cycle conversation detail", defaultKeyScope: "editor" }, "app.subagents.focus": { defaultKeys: "alt+a", description: "Open child agents", @@ -271,7 +257,6 @@ const KEYBINDING_NAME_MIGRATIONS = { suspend: "app.suspend", selectModel: "app.model.select", expandTools: "app.tools.expand", - toggleThinking: "app.thinking.toggle", focusSubagents: "app.subagents.focus", externalEditor: "app.editor.external", followUp: "app.message.followUp", diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 1fb87cbdb4..3039244be6 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -959,7 +959,8 @@ export class SettingsManager { } getHideThinkingBlock(): boolean { - return this.settings.hideThinkingBlock ?? false; + // Defaults to true: thinking rows stay hidden unless explicitly opted out. + return this.settings.hideThinkingBlock ?? true; } setHideThinkingBlock(hide: boolean): void { diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 9b72ffcc24..6143e6f98c 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -177,7 +177,7 @@ function formatDuration(ms: number): string { function formatBashCall(args: { command?: string; timeout?: number } | undefined): string { const command = str(args?.command); const timeout = args?.timeout as number | undefined; - const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : ""; + const timeoutSuffix = timeout ? theme.fg("dim", ` (timeout ${timeout}s)`) : ""; let commandDisplay: string; if (command === null) { commandDisplay = invalidArgText(theme); @@ -188,7 +188,7 @@ function formatBashCall(args: { command?: string; timeout?: number } | undefined } else { commandDisplay = theme.fg("toolOutput", "..."); } - return theme.fg("toolTitle", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix; + return theme.fg("dim", `$ ${commandDisplay}`) + timeoutSuffix; } function rebuildBashResultRenderComponent( @@ -228,8 +228,8 @@ function rebuildBashResultRenderComponent( } if (state.cachedSkipped && state.cachedSkipped > 0) { const hint = showExpandHint - ? `${theme.fg("muted", `... ${state.cachedSkipped} earlier lines`)} ${expandCollapseHint("app.tools.expand", false)}` - : theme.fg("muted", `... (${state.cachedSkipped} earlier lines)`); + ? `${theme.fg("dim", `... ${state.cachedSkipped} earlier lines`)} ${expandCollapseHint("app.tools.expand", false)}` + : theme.fg("dim", `... (${state.cachedSkipped} earlier lines)`); return ["", truncateToWidth(hint, width, "..."), ...(state.cachedLines ?? [])]; } return ["", ...(state.cachedLines ?? [])]; @@ -265,7 +265,7 @@ function rebuildBashResultRenderComponent( if (startedAt !== undefined) { const label = options.isPartial ? "Elapsed" : "Took"; const endTime = endedAt ?? Date.now(); - component.addChild(new Text(`\n${theme.fg("muted", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0)); + component.addChild(new Text(`\n${theme.fg("dim", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0)); } } diff --git a/packages/coding-agent/src/core/tools/code-preview.ts b/packages/coding-agent/src/core/tools/code-preview.ts index 0b2a42f67a..c7d25fb282 100644 --- a/packages/coding-agent/src/core/tools/code-preview.ts +++ b/packages/coding-agent/src/core/tools/code-preview.ts @@ -458,30 +458,32 @@ function scanPythonStringLiteral(code: string, start: number, quote: string, raw return { value, end: i, closed: false, unsupportedEscape }; } -// True when the lines end inside an unterminated triple-quoted string. -function endsInsideMultilineString(lines: readonly string[]): boolean { - const text = lines.join("\n"); +/** Keep source-line positions while excluding lines that begin inside string literals. */ +export function pythonStatementLines(code: string): string[] { + const lines = code.split("\n"); + let line = 0; let i = 0; - while (i < text.length) { - const char = text[i] ?? ""; + while (i < code.length) { + const char = code[i]!; if (char === "#") { - const newline = text.indexOf("\n", i); - if (newline < 0) return false; - i = newline + 1; + const newline = code.indexOf("\n", i); + if (newline < 0) break; + i = newline; continue; } if (char === '"' || char === "'") { - const quote = text.startsWith(char.repeat(3), i) ? char.repeat(3) : char; - const scan = scanPythonStringLiteral(text, i + quote.length, quote, true); - if (!scan.closed && scan.end >= text.length) { - return quote.length === 3; + const quote = code.startsWith(char.repeat(3), i) ? char.repeat(3) : char; + const scan = scanPythonStringLiteral(code, i + quote.length, quote, true); + for (let end = i; end < scan.end; end++) { + if (code[end] === "\n") lines[++line] = ""; } i = scan.end; continue; } - i += 1; + if (char === "\n") line++; + i++; } - return false; + return lines; } function extractBashSkillCommand(code: string): string | undefined { @@ -499,7 +501,8 @@ function extractBashSkillCommand(code: string): string | undefined { } export function previewPythonCode(code: string): CodePreview { - const lines = code.split("\n"); + const rawLines = code.split("\n"); + const lines = pythonStatementLines(code); const paths = pythonPathVars(lines); let bestIndex: number | undefined; let bestScore = -1; @@ -514,10 +517,8 @@ export function previewPythonCode(code: string): CodePreview { if (bestIndex !== undefined && bestScore >= 0) { const previewIndex = pythonPreviewIndex(lines, bestIndex); - // Extract from the full tail (literals may span lines), unless the chosen line is string text. - const bashCommand = endsInsideMultilineString(lines.slice(0, previewIndex)) - ? undefined - : extractBashSkillCommand(lines.slice(previewIndex).join("\n")); + // Extract from the original tail because a literal command may span lines. + const bashCommand = extractBashSkillCommand(rawLines.slice(previewIndex).join("\n")); if (bashCommand) { return previewBashCommand(bashCommand); } diff --git a/packages/coding-agent/src/core/tools/edit.ts b/packages/coding-agent/src/core/tools/edit.ts index df71f893f4..eee4c3424e 100644 --- a/packages/coding-agent/src/core/tools/edit.ts +++ b/packages/coding-agent/src/core/tools/edit.ts @@ -4,11 +4,7 @@ import { constants } from "fs"; import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises"; import { type Static, Type } from "typebox"; import { renderDiff } from "../../modes/interactive/components/diff.js"; -import { - countChangedLines, - FILE_CHANGE_DIFF_INDENT, - formatFileChangeSummaryLine, -} from "../../modes/interactive/components/edit-summary.js"; +import { countChangedLines, formatFileChangeSummaryLine } from "../../modes/interactive/components/edit-summary.js"; import type { ToolDefinition } from "../extensions/types.js"; import { applyEditsToNormalizedContent, @@ -202,8 +198,8 @@ function formatEditCall( const invalidArg = invalidArgText(theme); const rawPath = str(args?.file_path ?? args?.path); const path = rawPath !== null ? shortenPath(rawPath) : null; - const pathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."); - return `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`; + const pathDisplay = path === null ? invalidArg : path ? theme.fg("dim", path) : theme.fg("toolOutput", "..."); + return `${theme.fg("toolTitle", "edit")} ${pathDisplay}`; } function formatEditResult( @@ -249,8 +245,7 @@ function getEditHeaderBg( return (text: string) => theme.bg("toolPendingBg", text); } -// Width-aware `╰─ +N -M` summary plus optional indented diff rows: the -// summary truncates to one row and wrapped diff lines keep the indent column. +// Width-aware file summary with optional diff rows at the containing box's content margin. class EditChangeSummaryComponent implements Component { constructor( private readonly rawPath: string, @@ -264,11 +259,9 @@ class EditChangeSummaryComponent implements Component { const safeWidth = Math.max(1, width); const lines = [formatFileChangeSummaryLine(this.rawPath, this.cwd, this.change, this.diffsExpanded, safeWidth)]; if (this.diffLines !== undefined) { - const indent = FILE_CHANGE_DIFF_INDENT.slice(0, Math.max(0, safeWidth - 1)); - const contentWidth = Math.max(1, safeWidth - indent.length); for (const line of this.diffLines) { - for (const row of wrapTextWithAnsi(line, contentWidth)) { - lines.push(`${indent}${row}`); + for (const row of wrapTextWithAnsi(line, safeWidth)) { + lines.push(row); } } } @@ -299,8 +292,7 @@ function buildEditCallComponent( return component; } - // The `╰─ +N -M` summary line renders in both states; ctrl+j only - // attaches or removes the indented diff lines underneath it. + // Keep the file summary visible while detail expansion reveals the diff. const rawPath = str(args?.file_path ?? args?.path); const change = countChangedLines(component.preview.diff); component.addChild(new Spacer(1)); @@ -309,9 +301,6 @@ function buildEditCallComponent( rawPath ?? "...", cwd, change, - // The ctrl+j hint renders on every edit summary row (unlike the ctrl+o - // hint, which the latest tool row owns), matching thinking and - // agent-message hints. expanded, expanded ? renderDiff(component.preview.diff).split("\n") : undefined, ), diff --git a/packages/coding-agent/src/modes/interactive/components/agent-message.ts b/packages/coding-agent/src/modes/interactive/components/agent-message.ts index cf6845a495..80c805148f 100644 --- a/packages/coding-agent/src/modes/interactive/components/agent-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/agent-message.ts @@ -2,34 +2,22 @@ import { type Component, Container, type MarkdownTheme, - Spacer, Text, truncateToWidth, - visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui"; import { type AgentSessionMessage, formatAgentMessageParticipant } from "../../../core/agent-messages.js"; import { getMarkdownTheme, theme } from "../theme/theme.js"; -import { expandCollapseHint } from "./keybinding-hints.js"; - -function collapseText(text: string): string { - return text.replace(/\s+/g, " ").trim(); -} /** `◆