diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 2cd92782ed..6594500ad4 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -66,7 +66,7 @@ import { useConfig } from "../../config" import { useClipboard } from "../../context/clipboard" import { nextThinkingMode, reasoningSummary, type ThinkingMode } from "../../context/thinking" import { getScrollAcceleration } from "../../util/scroll" -import { collapseToolOutput } from "../../util/collapse-tool-output" +import { collapseToolOutput, normalizeShellOutput } from "../../util/collapse-tool-output" import { usePluginRuntime } from "../../plugin/runtime" import { PluginSlot } from "../../plugin/context" import { Keymap, type KeymapCommand } from "../../context/keymap" @@ -1607,21 +1607,24 @@ function RevertMessage(props: { function ShellMessage(props: { message: Extract }) { const { themeV2 } = useTheme().contextual("elevated") - const ctx = use() const output = createMemo(() => stripAnsi(props.message.output?.output.trim() ?? "")) - const [expanded, setExpanded] = createSignal(false) - const maxLines = 10 - const maxChars = createMemo(() => maxLines * Math.max(20, ctx.width - 6)) - const collapsed = createMemo(() => collapseToolOutput(output(), maxLines, maxChars())) - const limited = createMemo(() => (expanded() ? output() : collapsed().output)) return ( - setExpanded((value) => !value) : undefined}> + $ {props.message.command} - {limited()} + {output()} - + ) } @@ -2445,7 +2448,7 @@ function Shell(props: ToolProps) { limit: 1024 * 1024, location: location ? { directory: location.directory, workspace: location.workspaceID } : undefined, }) - .then((response) => setBackgroundOutput(stripAnsi(response.data.output.trim()))) + .then((response) => setBackgroundOutput(normalizeShellOutput(response.data.output.trim()))) .catch(() => undefined) loading = false } @@ -2458,7 +2461,7 @@ function Shell(props: ToolProps) { if (props.part.state.status === "streaming") return "" if (shellID()) return expanded() ? backgroundOutput() : "" const content = props.part.state.content[0] - return stripAnsi(content?.type === "text" ? content.text.trim() : "") + return normalizeShellOutput(content?.type === "text" ? content.text.trim() : "") }) const maxLines = 10 const maxChars = createMemo(() => maxLines * Math.max(20, ctx.width - 6)) diff --git a/packages/tui/src/util/collapse-tool-output.ts b/packages/tui/src/util/collapse-tool-output.ts index 7e2a534cd5..59d519ef9b 100644 --- a/packages/tui/src/util/collapse-tool-output.ts +++ b/packages/tui/src/util/collapse-tool-output.ts @@ -1,3 +1,5 @@ +import stripAnsi from "strip-ansi" + export function collapseToolOutput(output: string, maxLines: number, maxChars: number) { const lines = output.split("\n") if (lines.length <= maxLines && Array.from(output).length <= maxChars) { @@ -19,3 +21,7 @@ export function collapseToolOutput(output: string, maxLines: number, maxChars: n return { output: preview, overflow: true } } + +export function normalizeShellOutput(output: string) { + return stripAnsi(output).replace(/\r\n?/g, "\n") +} diff --git a/packages/tui/test/cli/tui/collapse-tool-output.test.ts b/packages/tui/test/cli/tui/collapse-tool-output.test.ts index f12697d5c8..f3ee404625 100644 --- a/packages/tui/test/cli/tui/collapse-tool-output.test.ts +++ b/packages/tui/test/cli/tui/collapse-tool-output.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import { collapseToolOutput } from "../../../src/util/collapse-tool-output" +import { collapseToolOutput, normalizeShellOutput } from "../../../src/util/collapse-tool-output" test("limits command input and output to the same line budget", () => { const command = Array.from({ length: 8 }, (_, index) => `command ${index + 1}`).join("\n") @@ -12,3 +12,13 @@ test("limits command input and output to the same line budget", () => { expect(collapsed.output).toContain("command 8\n\noutput 1…") expect(collapsed.output).not.toContain("output 2") }) + +test("normalizes carriage-return shell progress before collapsing", () => { + const output = Array.from({ length: 30 }, (_, index) => `progress ${index + 1}`).join("\r") + const collapsed = collapseToolOutput(normalizeShellOutput(output), 10, 1_000) + + expect(collapsed.overflow).toBe(true) + expect(collapsed.output.split("\n")).toHaveLength(10) + expect(collapsed.output).toContain("progress 10…") + expect(collapsed.output).not.toContain("progress 11") +})