From ef1371b3426a90e9e261cfa09adda7d9d8e39634 Mon Sep 17 00:00:00 2001 From: Yeamika <3491806852@qq.com> Date: Thu, 19 Mar 2026 01:52:54 +0800 Subject: [PATCH] add runtime TUI directory switching --- packages/opencode/src/cli/cmd/tui/app.tsx | 13 +++++++++ .../tui/component/dialog-change-directory.tsx | 28 +++++++++++++++++++ .../opencode/src/cli/cmd/tui/context/sdk.tsx | 16 +++++++++-- packages/opencode/src/cli/cmd/tui/thread.ts | 3 ++ packages/opencode/src/cli/cmd/tui/worker.ts | 15 ++++++++-- 5 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/component/dialog-change-directory.tsx diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 8bb17ff1336..80d9dc953ad 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -19,6 +19,7 @@ import { DialogThemeList } from "@tui/component/dialog-theme-list" import { DialogHelp } from "./ui/dialog-help" import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command" import { DialogAgent } from "@tui/component/dialog-agent" +import { DialogChangeDirectory } from "@tui/component/dialog-change-directory" import { DialogSessionList } from "@tui/component/dialog-session-list" import { DialogWorkspaceList } from "@tui/component/dialog-workspace-list" import { KeybindProvider } from "@tui/context/keybind" @@ -372,6 +373,18 @@ function App() { dialog.replace(() => ) }, }, + { + title: "Change directory", + value: "directory.change", + category: "Workspace", + slash: { + name: "changedirectory", + aliases: ["cd"], + }, + onSelect: () => { + dialog.replace(() => ) + }, + }, ...(Flag.OPENCODE_EXPERIMENTAL_WORKSPACES ? [ { diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-change-directory.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-change-directory.tsx new file mode 100644 index 00000000000..effe6e5903a --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-change-directory.tsx @@ -0,0 +1,28 @@ +import { useDialog } from "@tui/ui/dialog" +import { DialogPrompt } from "@tui/ui/dialog-prompt" +import { useRoute } from "@tui/context/route" +import { useSync } from "@tui/context/sync" +import { useSDK } from "../context/sdk" + +export function DialogChangeDirectory() { + const dialog = useDialog() + const route = useRoute() + const sync = useSync() + const sdk = useSDK() + + return ( + { + const dir = value.trim().replace(/\/+$/g, "") || "/" + dialog.clear() + sdk.setDirectory(dir) + route.navigate({ type: "home" }) + void sync.bootstrap() + }} + onCancel={() => dialog.clear()} + /> + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index 2403a4e938b..df9a7b8360f 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -5,6 +5,7 @@ import { batch, onCleanup, onMount } from "solid-js" export type EventSource = { on: (handler: (event: Event) => void) => () => void + setDirectory?: (directory: string) => void setWorkspace?: (workspaceID?: string) => void } @@ -18,6 +19,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ events?: EventSource }) => { const abort = new AbortController() + let directory = props.directory let workspaceID: string | undefined let sse: AbortController | undefined @@ -25,7 +27,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ return createOpencodeClient({ baseUrl: props.url, signal: abort.signal, - directory: props.directory, + directory, fetch: props.fetch, headers: props.headers, experimental_workspaceID: workspaceID, @@ -109,9 +111,19 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ get client() { return sdk }, - directory: props.directory, + get directory() { + return directory + }, event: emitter, fetch: props.fetch ?? fetch, + setDirectory(next: string) { + if (directory === next) return + directory = next + workspaceID = undefined + sdk = createSDK() + props.events?.setDirectory?.(next) + if (!props.events) startSSE() + }, setWorkspace(next?: string) { if (workspaceID === next) return workspaceID = next diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 6e787c7afdd..03e9ba74bc0 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -42,6 +42,9 @@ function createWorkerFetch(client: RpcClient): typeof fetch { function createEventSource(client: RpcClient): EventSource { return { on: (handler) => client.on("event", handler), + setDirectory: (directory) => { + void client.call("setDirectory", { directory }) + }, setWorkspace: (workspaceID) => { void client.call("setWorkspace", { workspaceID }) }, diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 408350c5209..3a9c589cf65 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -44,6 +44,11 @@ const eventStream = { abort: undefined as AbortController | undefined, } +const state = { + directory: process.cwd(), + workspaceID: undefined as string | undefined, +} + const startEventStream = (input: { directory: string; workspaceID?: string }) => { if (eventStream.abort) eventStream.abort.abort() const abort = new AbortController() @@ -96,7 +101,7 @@ const startEventStream = (input: { directory: string; workspaceID?: string }) => }) } -startEventStream({ directory: process.cwd() }) +startEventStream({ directory: state.directory }) export const rpc = { async fetch(input: { url: string; method: string; headers: Record; body?: string }) { @@ -136,8 +141,14 @@ export const rpc = { Config.global.reset() await Instance.disposeAll() }, + async setDirectory(input: { directory: string }) { + state.directory = input.directory + state.workspaceID = undefined + startEventStream({ directory: state.directory }) + }, async setWorkspace(input: { workspaceID?: string }) { - startEventStream({ directory: process.cwd(), workspaceID: input.workspaceID }) + state.workspaceID = input.workspaceID + startEventStream({ directory: state.directory, workspaceID: state.workspaceID }) }, async shutdown() { Log.Default.info("worker shutting down")