Skip to content

Commit f68f260

Browse files
committed
feat: tail-f effect for tool output — show last lines in TUI, auto-scroll in web UI
1 parent 1133d87 commit f68f260

File tree

2 files changed

+34
-9
lines changed

2 files changed

+34
-9
lines changed

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1656,7 +1656,7 @@ function Bash(props: ToolProps<typeof BashTool>) {
16561656
const overflow = createMemo(() => lines().length > 10)
16571657
const limited = createMemo(() => {
16581658
if (expanded() || !overflow()) return output()
1659-
return [...lines().slice(0, 10), "…"].join("\n")
1659+
return ["…", ...lines().slice(-10)].join("\n")
16601660
})
16611661

16621662
const workdirDisplay = createMemo(() => {

packages/ui/src/components/message-part.tsx

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { checksum } from "@opencode-ai/util/encode"
5050
import { Tooltip } from "./tooltip"
5151
import { IconButton } from "./icon-button"
5252
import { TextShimmer } from "./text-shimmer"
53+
import { createAutoScroll } from "../hooks"
5354

5455
interface Diagnostic {
5556
range: {
@@ -865,6 +866,27 @@ export interface ToolProps {
865866
locked?: boolean
866867
}
867868

869+
/**
870+
* A scrollable tool output container that auto-scrolls to the bottom
871+
* while the tool is running (tail -f effect). Respects user scroll —
872+
* if the user scrolls up, auto-scroll pauses until they scroll back down.
873+
*/
874+
function ScrollableToolOutput(props: { children: JSX.Element; status?: string }) {
875+
const autoScroll = createAutoScroll({
876+
working: () => props.status !== "completed" && props.status !== "error",
877+
})
878+
return (
879+
<div
880+
ref={autoScroll.scrollRef}
881+
onScroll={autoScroll.handleScroll}
882+
data-component="tool-output"
883+
data-scrollable
884+
>
885+
<div ref={autoScroll.contentRef}>{props.children}</div>
886+
</div>
887+
)
888+
}
889+
868890
export type ToolComponent = Component<ToolProps>
869891

870892
const state: Record<
@@ -1205,9 +1227,9 @@ ToolRegistry.register({
12051227
>
12061228
<Show when={props.output}>
12071229
{(output) => (
1208-
<div data-component="tool-output" data-scrollable>
1230+
<ScrollableToolOutput status={props.status}>
12091231
<Markdown text={output()} />
1210-
</div>
1232+
</ScrollableToolOutput>
12111233
)}
12121234
</Show>
12131235
</BasicTool>
@@ -1231,9 +1253,9 @@ ToolRegistry.register({
12311253
>
12321254
<Show when={props.output}>
12331255
{(output) => (
1234-
<div data-component="tool-output" data-scrollable>
1256+
<ScrollableToolOutput status={props.status}>
12351257
<Markdown text={output()} />
1236-
</div>
1258+
</ScrollableToolOutput>
12371259
)}
12381260
</Show>
12391261
</BasicTool>
@@ -1260,9 +1282,9 @@ ToolRegistry.register({
12601282
>
12611283
<Show when={props.output}>
12621284
{(output) => (
1263-
<div data-component="tool-output" data-scrollable>
1285+
<ScrollableToolOutput status={props.status}>
12641286
<Markdown text={output()} />
1265-
</div>
1287+
</ScrollableToolOutput>
12661288
)}
12671289
</Show>
12681290
</BasicTool>
@@ -1412,6 +1434,9 @@ ToolRegistry.register({
14121434
return `$ ${cmd}${out ? "\n\n" + out : ""}`
14131435
})
14141436
const [copied, setCopied] = createSignal(false)
1437+
const autoScroll = createAutoScroll({
1438+
working: () => props.status !== "completed" && props.status !== "error",
1439+
})
14151440

14161441
const handleCopy = async () => {
14171442
const content = text()
@@ -1447,8 +1472,8 @@ ToolRegistry.register({
14471472
/>
14481473
</Tooltip>
14491474
</div>
1450-
<div data-slot="bash-scroll" data-scrollable>
1451-
<pre data-slot="bash-pre">
1475+
<div ref={autoScroll.scrollRef} onScroll={autoScroll.handleScroll} data-slot="bash-scroll" data-scrollable>
1476+
<pre ref={autoScroll.contentRef} data-slot="bash-pre">
14521477
<code>{text()}</code>
14531478
</pre>
14541479
</div>

0 commit comments

Comments
 (0)