fix(tui): preserve shell preview boundaries

This commit is contained in:
Kit Langton
2026-07-16 14:05:41 -04:00
parent befd29212c
commit 7d911b824d
3 changed files with 58 additions and 11 deletions
+4 -10
View File
@@ -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, collapseToolOutputParts } from "../../util/collapse-tool-output"
import { usePluginRuntime } from "../../plugin/runtime"
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut } from "../../keymap"
import { usePathFormatter } from "../../context/path-format"
@@ -2384,16 +2384,10 @@ function Shell(props: ToolProps) {
const maxLines = 10
const maxChars = createMemo(() => maxLines * Math.max(20, ctx.width - 6))
const input = createMemo(() => (command() ? `${isRunning() ? "" : "$ "}${command()}` : ""))
const content = createMemo(() => [input(), output()].filter(Boolean).join("\n\n"))
const collapsed = createMemo(() => collapseToolOutput(content(), maxLines, maxChars()))
const collapsed = createMemo(() => collapseToolOutputParts(input(), output(), maxLines, maxChars()))
const visible = createMemo(() => {
const command = input()
if (expanded()) return { input: command, output: output() }
const preview = collapsed().overflow ? collapsed().output : content()
return {
input: preview.slice(0, command.length),
output: preview.slice(command.length).replace(/^\n+/, ""),
}
if (expanded()) return { input: input(), output: output() }
return collapsed()
})
const expandable = createMemo(() => Boolean(shellID()) || collapsed().overflow)
const toggle = () => {
@@ -19,3 +19,26 @@ export function collapseToolOutput(output: string, maxLines: number, maxChars: n
return { output: preview, overflow: true }
}
export function collapseToolOutputParts(input: string, output: string, maxLines: number, maxChars: number) {
const separator = input && output ? "\n\n" : ""
const collapsed = collapseToolOutput(`${input}${separator}${output}`, maxLines, maxChars)
if (!collapsed.overflow) return { input, output, overflow: false }
const ellipsis = collapsed.output.endsWith("…") ? "…" : ""
const preview = ellipsis ? collapsed.output.slice(0, -1) : collapsed.output
const outputStart = input.length + separator.length
if (preview.length <= outputStart) {
return {
input: preview.slice(0, input.length) + ellipsis,
output: "",
overflow: true,
}
}
return {
input,
output: preview.slice(outputStart) + ellipsis,
overflow: true,
}
}
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test"
import { collapseToolOutput } from "../../../src/util/collapse-tool-output"
import { collapseToolOutput, collapseToolOutputParts } 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,33 @@ 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.each([
{
name: "inside the command",
maxChars: 5,
expected: { input: "$ co…", output: "", overflow: true },
},
{
name: "after the command",
maxChars: 10,
expected: { input: "$ command…", output: "", overflow: true },
},
{
name: "on the first separator newline",
maxChars: 11,
expected: { input: "$ command…", output: "", overflow: true },
},
{
name: "on the second separator newline",
maxChars: 12,
expected: { input: "$ command…", output: "", overflow: true },
},
{
name: "inside the output",
maxChars: 15,
expected: { input: "$ command", output: "out…", overflow: true },
},
])("keeps the ellipsis with the visible $name", ({ maxChars, expected }) => {
expect(collapseToolOutputParts("$ command", "output", 10, maxChars)).toEqual(expected)
})