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")