fix(tui): collapse long tool output lines (#28148)
This commit is contained in:
@@ -30,6 +30,7 @@ import type {
|
|||||||
ToolTextContent,
|
ToolTextContent,
|
||||||
} from "@opencode-ai/sdk/v2"
|
} from "@opencode-ai/sdk/v2"
|
||||||
import { createEffect, createMemo, createSignal, For, Match, Show, Switch } from "solid-js"
|
import { createEffect, createMemo, createSignal, For, Match, Show, Switch } from "solid-js"
|
||||||
|
import { collapseToolOutput } from "../../util/collapse-tool-output"
|
||||||
|
|
||||||
const id = "internal:session-v2-debug"
|
const id = "internal:session-v2-debug"
|
||||||
const route = "session.v2.messages"
|
const route = "session.v2.messages"
|
||||||
@@ -198,26 +199,28 @@ function UserMessage(props: { message: SessionMessageUser; index: number }) {
|
|||||||
|
|
||||||
function ShellMessage(props: { message: SessionMessageShell }) {
|
function ShellMessage(props: { message: SessionMessageShell }) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const dimensions = useTerminalDimensions()
|
||||||
const output = createMemo(() => stripAnsi(props.message.output.trim()))
|
const output = createMemo(() => stripAnsi(props.message.output.trim()))
|
||||||
const [expanded, setExpanded] = createSignal(false)
|
const [expanded, setExpanded] = createSignal(false)
|
||||||
const lines = createMemo(() => output().split("\n"))
|
const maxLines = 10
|
||||||
const overflow = createMemo(() => lines().length > 10)
|
const maxChars = createMemo(() => maxLines * Math.max(20, dimensions().width - 6))
|
||||||
|
const collapsed = createMemo(() => collapseToolOutput(output(), maxLines, maxChars()))
|
||||||
const limited = createMemo(() => {
|
const limited = createMemo(() => {
|
||||||
if (expanded() || !overflow()) return output()
|
if (expanded() || !collapsed().overflow) return output()
|
||||||
return [...lines().slice(0, 10), "…"].join("\n")
|
return collapsed().output
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<BlockTool
|
<BlockTool
|
||||||
title="# Shell"
|
title="# Shell"
|
||||||
spinner={!props.message.time.completed}
|
spinner={!props.message.time.completed}
|
||||||
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
|
onClick={collapsed().overflow ? () => setExpanded((prev) => !prev) : undefined}
|
||||||
>
|
>
|
||||||
<box gap={1}>
|
<box gap={1}>
|
||||||
<text fg={theme.text}>$ {props.message.command}</text>
|
<text fg={theme.text}>$ {props.message.command}</text>
|
||||||
<Show when={output()}>
|
<Show when={output()}>
|
||||||
<text fg={theme.text}>{limited()}</text>
|
<text fg={theme.text}>{limited()}</text>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={overflow()}>
|
<Show when={collapsed().overflow}>
|
||||||
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
@@ -518,14 +521,15 @@ type ToolProps = {
|
|||||||
|
|
||||||
function GenericTool(props: ToolProps) {
|
function GenericTool(props: ToolProps) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const dimensions = useTerminalDimensions()
|
||||||
const output = createMemo(() => props.output?.trim() ?? "")
|
const output = createMemo(() => props.output?.trim() ?? "")
|
||||||
const [expanded, setExpanded] = createSignal(false)
|
const [expanded, setExpanded] = createSignal(false)
|
||||||
const lines = createMemo(() => output().split("\n"))
|
|
||||||
const maxLines = 3
|
const maxLines = 3
|
||||||
const overflow = createMemo(() => lines().length > maxLines)
|
const maxChars = createMemo(() => maxLines * Math.max(20, dimensions().width - 6))
|
||||||
|
const collapsed = createMemo(() => collapseToolOutput(output(), maxLines, maxChars()))
|
||||||
const limited = createMemo(() => {
|
const limited = createMemo(() => {
|
||||||
if (expanded() || !overflow()) return output()
|
if (expanded() || !collapsed().overflow) return output()
|
||||||
return [...lines().slice(0, maxLines), "…"].join("\n")
|
return collapsed().output
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
@@ -539,11 +543,11 @@ function GenericTool(props: ToolProps) {
|
|||||||
<BlockTool
|
<BlockTool
|
||||||
title={`# ${props.part.name} ${input(props.input)}`}
|
title={`# ${props.part.name} ${input(props.input)}`}
|
||||||
part={props.part}
|
part={props.part}
|
||||||
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
|
onClick={collapsed().overflow ? () => setExpanded((prev) => !prev) : undefined}
|
||||||
>
|
>
|
||||||
<box gap={1}>
|
<box gap={1}>
|
||||||
<text fg={theme.text}>{limited()}</text>
|
<text fg={theme.text}>{limited()}</text>
|
||||||
<Show when={overflow()}>
|
<Show when={collapsed().overflow}>
|
||||||
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
@@ -702,15 +706,17 @@ function BlockTool(props: {
|
|||||||
|
|
||||||
function Bash(props: ToolProps) {
|
function Bash(props: ToolProps) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const dimensions = useTerminalDimensions()
|
||||||
const output = createMemo(() => stripAnsi((stringValue(props.metadata.output) ?? props.output ?? "").trim()))
|
const output = createMemo(() => stripAnsi((stringValue(props.metadata.output) ?? props.output ?? "").trim()))
|
||||||
const command = createMemo(() => stringValue(props.input.command) ?? pendingInput(props.part))
|
const command = createMemo(() => stringValue(props.input.command) ?? pendingInput(props.part))
|
||||||
const title = createMemo(() => `# ${stringValue(props.input.description) ?? "Shell"}`)
|
const title = createMemo(() => `# ${stringValue(props.input.description) ?? "Shell"}`)
|
||||||
const [expanded, setExpanded] = createSignal(false)
|
const [expanded, setExpanded] = createSignal(false)
|
||||||
const lines = createMemo(() => output().split("\n"))
|
const maxLines = 10
|
||||||
const overflow = createMemo(() => lines().length > 10)
|
const maxChars = createMemo(() => maxLines * Math.max(20, dimensions().width - 6))
|
||||||
|
const collapsed = createMemo(() => collapseToolOutput(output(), maxLines, maxChars()))
|
||||||
const limited = createMemo(() => {
|
const limited = createMemo(() => {
|
||||||
if (expanded() || !overflow()) return output()
|
if (expanded() || !collapsed().overflow) return output()
|
||||||
return [...lines().slice(0, 10), "…"].join("\n")
|
return collapsed().output
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -719,12 +725,12 @@ function Bash(props: ToolProps) {
|
|||||||
title={title()}
|
title={title()}
|
||||||
part={props.part}
|
part={props.part}
|
||||||
spinner={props.part.state.status === "running"}
|
spinner={props.part.state.status === "running"}
|
||||||
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
|
onClick={collapsed().overflow ? () => setExpanded((prev) => !prev) : undefined}
|
||||||
>
|
>
|
||||||
<box gap={1}>
|
<box gap={1}>
|
||||||
<text fg={theme.text}>$ {command()}</text>
|
<text fg={theme.text}>$ {command()}</text>
|
||||||
<text fg={theme.text}>{limited()}</text>
|
<text fg={theme.text}>{limited()}</text>
|
||||||
<Show when={overflow()}>
|
<Show when={collapsed().overflow}>
|
||||||
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ import { UI } from "@/cli/ui.ts"
|
|||||||
import { useTuiConfig } from "../../context/tui-config"
|
import { useTuiConfig } from "../../context/tui-config"
|
||||||
import { nextThinkingMode, reasoningTitle, useThinkingMode, type ThinkingMode } from "../../context/thinking"
|
import { nextThinkingMode, reasoningTitle, useThinkingMode, type ThinkingMode } from "../../context/thinking"
|
||||||
import { getScrollAcceleration } from "../../util/scroll"
|
import { getScrollAcceleration } from "../../util/scroll"
|
||||||
|
import { collapseToolOutput } from "../../util/collapse-tool-output"
|
||||||
import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
|
import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
|
||||||
import { DialogRetryAction } from "../../component/dialog-retry-action"
|
import { DialogRetryAction } from "../../component/dialog-retry-action"
|
||||||
import { SessionRetry } from "@/session/retry"
|
import { SessionRetry } from "@/session/retry"
|
||||||
@@ -1696,12 +1697,12 @@ function GenericTool(props: ToolProps<any>) {
|
|||||||
const ctx = use()
|
const ctx = use()
|
||||||
const output = createMemo(() => props.output?.trim() ?? "")
|
const output = createMemo(() => props.output?.trim() ?? "")
|
||||||
const [expanded, setExpanded] = createSignal(false)
|
const [expanded, setExpanded] = createSignal(false)
|
||||||
const lines = createMemo(() => output().split("\n"))
|
|
||||||
const maxLines = 3
|
const maxLines = 3
|
||||||
const overflow = createMemo(() => lines().length > maxLines)
|
const maxChars = createMemo(() => maxLines * Math.max(20, ctx.width - 6))
|
||||||
|
const collapsed = createMemo(() => collapseToolOutput(output(), maxLines, maxChars()))
|
||||||
const limited = createMemo(() => {
|
const limited = createMemo(() => {
|
||||||
if (expanded() || !overflow()) return output()
|
if (expanded() || !collapsed().overflow) return output()
|
||||||
return [...lines().slice(0, maxLines), "…"].join("\n")
|
return collapsed().output
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1716,11 +1717,11 @@ function GenericTool(props: ToolProps<any>) {
|
|||||||
<BlockTool
|
<BlockTool
|
||||||
title={`# ${props.tool} ${input(props.input)}`}
|
title={`# ${props.tool} ${input(props.input)}`}
|
||||||
part={props.part}
|
part={props.part}
|
||||||
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
|
onClick={collapsed().overflow ? () => setExpanded((prev) => !prev) : undefined}
|
||||||
>
|
>
|
||||||
<box gap={1}>
|
<box gap={1}>
|
||||||
<text fg={theme.text}>{limited()}</text>
|
<text fg={theme.text}>{limited()}</text>
|
||||||
<Show when={overflow()}>
|
<Show when={collapsed().overflow}>
|
||||||
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
@@ -1871,14 +1872,16 @@ function BlockTool(props: {
|
|||||||
function Shell(props: ToolProps<typeof ShellTool>) {
|
function Shell(props: ToolProps<typeof ShellTool>) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const pathFormatter = usePathFormatter()
|
const pathFormatter = usePathFormatter()
|
||||||
|
const ctx = use()
|
||||||
const isRunning = createMemo(() => props.part.state.status === "running")
|
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||||
const output = createMemo(() => stripAnsi(props.metadata.output?.trim() ?? ""))
|
const output = createMemo(() => stripAnsi(props.metadata.output?.trim() ?? ""))
|
||||||
const [expanded, setExpanded] = createSignal(false)
|
const [expanded, setExpanded] = createSignal(false)
|
||||||
const lines = createMemo(() => output().split("\n"))
|
const maxLines = 10
|
||||||
const overflow = createMemo(() => lines().length > 10)
|
const maxChars = createMemo(() => maxLines * Math.max(20, ctx.width - 6))
|
||||||
|
const collapsed = createMemo(() => collapseToolOutput(output(), maxLines, maxChars()))
|
||||||
const limited = createMemo(() => {
|
const limited = createMemo(() => {
|
||||||
if (expanded() || !overflow()) return output()
|
if (expanded() || !collapsed().overflow) return output()
|
||||||
return [...lines().slice(0, 10), "…"].join("\n")
|
return collapsed().output
|
||||||
})
|
})
|
||||||
|
|
||||||
const workdirDisplay = createMemo(() => {
|
const workdirDisplay = createMemo(() => {
|
||||||
@@ -1902,14 +1905,14 @@ function Shell(props: ToolProps<typeof ShellTool>) {
|
|||||||
title={title()}
|
title={title()}
|
||||||
part={props.part}
|
part={props.part}
|
||||||
spinner={isRunning()}
|
spinner={isRunning()}
|
||||||
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
|
onClick={collapsed().overflow ? () => setExpanded((prev) => !prev) : undefined}
|
||||||
>
|
>
|
||||||
<box gap={1}>
|
<box gap={1}>
|
||||||
<text fg={theme.text}>$ {props.input.command}</text>
|
<text fg={theme.text}>$ {props.input.command}</text>
|
||||||
<Show when={output()}>
|
<Show when={output()}>
|
||||||
<text fg={theme.text}>{limited()}</text>
|
<text fg={theme.text}>{limited()}</text>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={overflow()}>
|
<Show when={collapsed().overflow}>
|
||||||
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export function collapseToolOutput(output: string, maxLines: number, maxChars: number) {
|
||||||
|
const lines = output.split("\n")
|
||||||
|
if (lines.length <= maxLines && Array.from(output).length <= maxChars) {
|
||||||
|
return { output, overflow: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const preview = lines.slice(0, maxLines).join("\n")
|
||||||
|
if (Array.from(preview).length > maxChars) {
|
||||||
|
return { output: Array.from(preview).slice(0, Math.max(0, maxChars - 1)).join("") + "…", overflow: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { output: [...lines.slice(0, maxLines), "…"].join("\n"), overflow: true }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user