Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1656,7 +1656,7 @@ function Bash(props: ToolProps<typeof BashTool>) {
const overflow = createMemo(() => lines().length > 10)
const limited = createMemo(() => {
if (expanded() || !overflow()) return output()
return [...lines().slice(0, 10), "…"].join("\n")
return ["…", ...lines().slice(-10)].join("\n")
})

const workdirDisplay = createMemo(() => {
Expand Down
41 changes: 33 additions & 8 deletions packages/ui/src/components/message-part.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { checksum } from "@opencode-ai/util/encode"
import { Tooltip } from "./tooltip"
import { IconButton } from "./icon-button"
import { TextShimmer } from "./text-shimmer"
import { createAutoScroll } from "../hooks"

interface Diagnostic {
range: {
Expand Down Expand Up @@ -865,6 +866,27 @@ export interface ToolProps {
locked?: boolean
}

/**
* A scrollable tool output container that auto-scrolls to the bottom
* while the tool is running (tail -f effect). Respects user scroll —
* if the user scrolls up, auto-scroll pauses until they scroll back down.
*/
function ScrollableToolOutput(props: { children: JSX.Element; status?: string }) {
const autoScroll = createAutoScroll({
working: () => props.status !== "completed" && props.status !== "error",
})
return (
<div
ref={autoScroll.scrollRef}
onScroll={autoScroll.handleScroll}
data-component="tool-output"
data-scrollable
>
<div ref={autoScroll.contentRef}>{props.children}</div>
</div>
)
}

export type ToolComponent = Component<ToolProps>

const state: Record<
Expand Down Expand Up @@ -1205,9 +1227,9 @@ ToolRegistry.register({
>
<Show when={props.output}>
{(output) => (
<div data-component="tool-output" data-scrollable>
<ScrollableToolOutput status={props.status}>
<Markdown text={output()} />
</div>
</ScrollableToolOutput>
)}
</Show>
</BasicTool>
Expand All @@ -1231,9 +1253,9 @@ ToolRegistry.register({
>
<Show when={props.output}>
{(output) => (
<div data-component="tool-output" data-scrollable>
<ScrollableToolOutput status={props.status}>
<Markdown text={output()} />
</div>
</ScrollableToolOutput>
)}
</Show>
</BasicTool>
Expand All @@ -1260,9 +1282,9 @@ ToolRegistry.register({
>
<Show when={props.output}>
{(output) => (
<div data-component="tool-output" data-scrollable>
<ScrollableToolOutput status={props.status}>
<Markdown text={output()} />
</div>
</ScrollableToolOutput>
)}
</Show>
</BasicTool>
Expand Down Expand Up @@ -1412,6 +1434,9 @@ ToolRegistry.register({
return `$ ${cmd}${out ? "\n\n" + out : ""}`
})
const [copied, setCopied] = createSignal(false)
const autoScroll = createAutoScroll({
working: () => props.status !== "completed" && props.status !== "error",
})

const handleCopy = async () => {
const content = text()
Expand Down Expand Up @@ -1447,8 +1472,8 @@ ToolRegistry.register({
/>
</Tooltip>
</div>
<div data-slot="bash-scroll" data-scrollable>
<pre data-slot="bash-pre">
<div ref={autoScroll.scrollRef} onScroll={autoScroll.handleScroll} data-slot="bash-scroll" data-scrollable>
<pre ref={autoScroll.contentRef} data-slot="bash-pre">
<code>{text()}</code>
</pre>
</div>
Expand Down
Loading