diff --git a/packages/app/src/shell/routes/session-ui-provider.tsx b/packages/app/src/shell/routes/session-ui-provider.tsx index d0c6366207c..ec60583e4d3 100644 --- a/packages/app/src/shell/routes/session-ui-provider.tsx +++ b/packages/app/src/shell/routes/session-ui-provider.tsx @@ -6,6 +6,7 @@ import { LocalProvider } from "@/providers/models/selection" import type { ServerConnection } from "@/runtime/server/registry" import { sessionHref } from "@/shell/routes/session" import { useData } from "@/runtime/server/current" +import { useServerSDK } from "@/runtime/server/client" import { useTabs } from "@/shell/tabs/tabs" export function SessionUIProvider( @@ -17,6 +18,7 @@ export function SessionUIProvider( const navigate = useNavigate() const params = useParams() const data = useData() + const serverSDK = useServerSDK() const tabs = useTabs() const directory = () => props.directory const href = (sessionID: string) => sessionHref(props.server, sessionID) @@ -53,6 +55,7 @@ export function SessionUIProvider( data={sessionUIData()} directory={directory()} sessionID={params.id} + shellOutput={(input) => serverSDK.api.shell.output(input)} onNavigateToSession={navigateToSession} onSessionHref={href} > diff --git a/packages/session-ui/src/context/data.tsx b/packages/session-ui/src/context/data.tsx index 49de9f7a8be..5b7a5e8c036 100644 --- a/packages/session-ui/src/context/data.tsx +++ b/packages/session-ui/src/context/data.tsx @@ -1,4 +1,10 @@ -import type { FileDiffInfo, SessionInfo, SessionStatus } from "@opencode-ai/client/promise" +import type { + FileDiffInfo, + SessionInfo, + SessionStatus, + ShellOutputInput, + ShellOutputOutput, +} from "@opencode-ai/client/promise" import { createSimpleContext } from "@opencode-ai/ui/context" import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" @@ -40,6 +46,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ data: Data directory: string sessionID?: string + shellOutput?: (input: ShellOutputInput) => Promise onNavigateToSession?: NavigateToSessionFn onSessionHref?: SessionHrefFn }) => { @@ -55,6 +62,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ }, navigateToSession: props.onNavigateToSession, sessionHref: props.onSessionHref, + shellOutput: props.shellOutput, } }, }) diff --git a/packages/session-ui/src/tools/tool-renderer.tsx b/packages/session-ui/src/tools/tool-renderer.tsx index ac3dcdeecfb..d63dc6cf434 100644 --- a/packages/session-ui/src/tools/tool-renderer.tsx +++ b/packages/session-ui/src/tools/tool-renderer.tsx @@ -6,6 +6,7 @@ import { createSignal, For, Match, + onCleanup, onMount, Show, Switch, @@ -1294,15 +1295,41 @@ ToolRegistry.register({ name: "shell", render(props) { const i18n = useI18n() + const data = useData() const streaming = () => props.status === "streaming" const pending = () => streaming() || props.status === "running" || props.metadata.status === "running" const sawStreaming = streaming() + const [streamed, setStreamed] = createSignal("") + createEffect(() => { + const id = props.metadata.shellID + const shellOutput = data.shellOutput + if (typeof id !== "string" || !pending() || !shellOutput) return + const directory = data.directory + let cursor = 0 + let loading = false + let disposed = false + const load = async () => { + if (loading) return + loading = true + const response = await shellOutput({ id, location: { directory }, cursor }).catch(() => undefined) + if (disposed) return + if (response?.data.output) setStreamed((output) => output + response.data.output) + if (response) cursor = response.data.cursor + loading = false + } + void load() + const interval = setInterval(() => void load(), 1_000) + onCleanup(() => { + disposed = true + clearInterval(interval) + }) + }) const command = () => { if (typeof props.input.command === "string") return props.input.command if (typeof props.metadata.command === "string") return props.metadata.command return "" } - const output = createMemo(() => stripAnsi(props.output ?? "").replace(/\r\n?/g, "\n")) + const output = createMemo(() => stripAnsi((pending() && streamed()) || props.output || "").replace(/\r\n?/g, "\n")) return (