diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 971f76e8df5..dbd94916298 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -93,6 +93,8 @@ const context = createContext<{ showDetails: () => boolean diffWrapMode: () => "word" | "none" sync: ReturnType + toolExpanded: (partId: string) => boolean + toggleToolExpanded: (partId: string) => void }>() function use() { @@ -893,7 +895,22 @@ export function Session() { const dialog = useDialog() const renderer = useRenderer() - // snap to bottom when session changes + const [expandedToolPartIds, setExpandedToolPartIds] = createSignal>(new Set()) + + const toolExpanded = (partId: string) => expandedToolPartIds().has(partId) + + const toggleToolExpanded = (partId: string) => { + setExpandedToolPartIds((prev) => { + const next = new Set(prev) + if (next.has(partId)) { + next.delete(partId) + } else { + next.add(partId) + } + return next + }) + } + createEffect(on(() => route.sessionID, toBottom)) return ( @@ -910,6 +927,8 @@ export function Session() { showDetails, diffWrapMode, sync, + toolExpanded, + toggleToolExpanded, }} > @@ -1459,11 +1478,27 @@ function InlineTool(props: { icon: string; complete: any; pending: string; child ) } -function BlockTool(props: { title: string; children: JSX.Element; onClick?: () => void; part?: ToolPart }) { +function BlockTool(props: { + title: string + children: JSX.Element + onClick?: () => void + onExpand?: () => void + part?: ToolPart +}) { const { theme } = useTheme() const renderer = useRenderer() + const keybind = useKeybind() const [hover, setHover] = createSignal(false) const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error : undefined)) + + useKeyboard((evt) => { + if (!hover()) return + if (evt.name === "o" && props.onExpand) { + evt.preventDefault() + props.onExpand() + } + }) + return ( props.onClick && setHover(true)} + onMouseOver={() => setHover(true)} onMouseOut={() => setHover(false)} onMouseUp={() => { if (renderer.getSelection()?.getSelectedText()) return - props.onClick?.() + if (props.onExpand) { + props.onExpand() + } else { + props.onClick?.() + } }} > @@ -1494,15 +1533,48 @@ function BlockTool(props: { title: string; children: JSX.Element; onClick?: () = } function Bash(props: ToolProps) { + const ctx = use() const output = createMemo(() => stripAnsi(props.metadata.output?.trim() ?? "")) const { theme } = useTheme() + + const MAX_LINES = 5 + + const outputLines = createMemo(() => output().split("\n")) + const isLongOutput = createMemo(() => outputLines().length > MAX_LINES) + const isExpanded = createMemo(() => ctx.toolExpanded(props.part.id)) + + const displayOutput = createMemo(() => { + if (!ctx.showDetails() && !isExpanded()) { + if (isLongOutput()) { + return outputLines().slice(0, MAX_LINES).join("\n") + } + } + return output() + }) + + const hiddenLines = createMemo(() => { + if (!ctx.showDetails() && !isExpanded() && isLongOutput()) { + return outputLines().length - MAX_LINES + } + return 0 + }) + return ( - + ctx.toggleToolExpanded(props.part.id)} + > - $ {props.input.command} - {output()} + + $ {props.input.command} + + {displayOutput()} + 0}> + ... +{hiddenLines()} lines (press 'o' to expand) +