diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index a80039dd2e..857c76c787 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -289,9 +289,6 @@ function App(props: { onSnapshot?: () => Promise }) { toast, renderer, }) - onCleanup(() => { - api.dispose() - }) const [ready, setReady] = createSignal(false) TuiPluginRuntime.init(api) .catch((error) => { diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index a0f1b32249..348c3ca1db 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -4,8 +4,7 @@ import { createGlobalEmitter } from "@solid-primitives/event-bus" import { batch, onCleanup, onMount } from "solid-js" export type EventSource = { - on: (handler: (event: Event) => void) => () => void - setWorkspace?: (workspaceID?: string) => void + subscribe: (directory: string | undefined, handler: (event: Event) => void) => Promise<() => void> } export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ @@ -18,7 +17,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ events?: EventSource }) => { const abort = new AbortController() - let workspaceID: string | undefined let sse: AbortController | undefined function createSDK() { @@ -28,7 +26,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ directory: props.directory, fetch: props.fetch, headers: props.headers, - experimental_workspaceID: workspaceID, }) } @@ -90,9 +87,9 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ })().catch(() => {}) } - onMount(() => { + onMount(async () => { if (props.events) { - const unsub = props.events.on(handleEvent) + const unsub = await props.events.subscribe(props.directory, handleEvent) onCleanup(unsub) } else { startSSE() @@ -109,19 +106,9 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ get client() { return sdk }, - get workspaceID() { - return workspaceID - }, directory: props.directory, event: emitter, fetch: props.fetch ?? fetch, - setWorkspace(next?: string) { - if (workspaceID === next) return - workspaceID = next - sdk = createSDK() - props.events?.setWorkspace?.(next) - if (!props.events) startSSE() - }, url: props.url, } }, diff --git a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx index 529c50cfa3..3609f6cc1a 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx +++ b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx @@ -18,7 +18,7 @@ import { Prompt } from "../component/prompt" import { Slot as HostSlot } from "./slots" import type { useToast } from "../ui/toast" import { Installation } from "@/installation" -import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2" +import { type OpencodeClient } from "@opencode-ai/sdk/v2" type RouteEntry = { key: symbol @@ -43,11 +43,6 @@ type Input = { renderer: TuiPluginApi["renderer"] } -type TuiHostPluginApi = TuiPluginApi & { - map: Map - dispose: () => void -} - function routeRegister(routes: RouteMap, list: TuiRouteDefinition[], bump: () => void) { const key = Symbol() for (const item of list) { @@ -206,29 +201,7 @@ function appApi(): TuiPluginApi["app"] { } } -export function createTuiApi(input: Input): TuiHostPluginApi { - const map = new Map() - const scoped: TuiPluginApi["scopedClient"] = (workspaceID) => { - const hit = map.get(workspaceID) - if (hit) return hit - - const next = createOpencodeClient({ - baseUrl: input.sdk.url, - fetch: input.sdk.fetch, - directory: input.sync.data.path.directory || input.sdk.directory, - experimental_workspaceID: workspaceID, - }) - map.set(workspaceID, next) - return next - } - const workspace: TuiPluginApi["workspace"] = { - current() { - return input.sdk.workspaceID - }, - set(workspaceID) { - input.sdk.setWorkspace(workspaceID) - }, - } +export function createTuiApi(input: Input): TuiPluginApi { const lifecycle: TuiPluginApi["lifecycle"] = { signal: new AbortController().signal, onDispose() { @@ -369,8 +342,6 @@ export function createTuiApi(input: Input): TuiHostPluginApi { get client() { return input.sdk.client }, - scopedClient: scoped, - workspace, event: input.sdk.event, renderer: input.renderer, slots: { @@ -422,9 +393,5 @@ export function createTuiApi(input: Input): TuiHostPluginApi { return input.theme.ready }, }, - map, - dispose() { - map.clear() - }, } } diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index b33efdbd36..2f7fd51643 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -543,8 +543,6 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop get client() { return api.client }, - scopedClient: api.scopedClient, - workspace: api.workspace, event, renderer: api.renderer, slots, diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index afeed6a22f..91baca52a2 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -167,12 +167,6 @@ export function Session() { const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig)) - createEffect(() => { - if (session()?.workspaceID) { - sdk.setWorkspace(session()?.workspaceID) - } - }) - createEffect(async () => { await sync.session .sync(route.sessionID) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 3bb56937a6..df5c416777 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -43,9 +43,18 @@ function createWorkerFetch(client: RpcClient): typeof fetch { function createEventSource(client: RpcClient): EventSource { return { - on: (handler) => client.on("event", handler), - setWorkspace: (workspaceID) => { - void client.call("setWorkspace", { workspaceID }) + subscribe: async (directory, handler) => { + const id = await client.call("subscribe", { directory }) + const unsub = client.on<{ id: string; event: Event }>("event", (e) => { + if (e.id === id) { + handler(e.event) + } + }) + + return () => { + unsub() + client.call("unsubscribe", { id }) + } }, } } diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 643676e348..0b9ec82dc7 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -45,20 +45,20 @@ GlobalBus.on("event", (event) => { let server: Awaited> | undefined -const eventStream = { - abort: undefined as AbortController | undefined, -} +const eventStreams = new Map() + +function startEventStream(directory: string) { + const id = crypto.randomUUID() -const startEventStream = (input: { directory: string; workspaceID?: string }) => { - if (eventStream.abort) eventStream.abort.abort() const abort = new AbortController() - eventStream.abort = abort const signal = abort.signal - ;(async () => { + eventStreams.set(id, abort) + + async function run() { while (!signal.aborted) { const shouldReconnect = await Instance.provide({ - directory: input.directory, + directory, init: InstanceBootstrap, fn: () => new Promise((resolve) => { @@ -77,7 +77,10 @@ const startEventStream = (input: { directory: string; workspaceID?: string }) => } const unsub = Bus.subscribeAll((event) => { - Rpc.emit("event", event as Event) + Rpc.emit("event", { + id, + event: event as Event, + }) if (event.type === Bus.InstanceDisposed.type) { settle(true) } @@ -104,14 +107,24 @@ const startEventStream = (input: { directory: string; workspaceID?: string }) => await sleep(250) } } - })().catch((error) => { + } + + run().catch((error) => { Log.Default.error("event stream error", { error: error instanceof Error ? error.message : error, }) }) + + return id } -startEventStream({ directory: process.cwd() }) +function stopEventStream(id: string) { + const abortController = eventStreams.get(id) + if (!abortController) return + + abortController.abort() + eventStreams.delete(id) +} export const rpc = { async fetch(input: { url: string; method: string; headers: Record; body?: string }) { @@ -154,12 +167,19 @@ export const rpc = { async reload() { await Config.invalidate(true) }, - async setWorkspace(input: { workspaceID?: string }) { - startEventStream({ directory: process.cwd(), workspaceID: input.workspaceID }) + async subscribe(input: { directory: string | undefined }) { + return startEventStream(input.directory || process.cwd()) + }, + async unsubscribe(input: { id: string }) { + stopEventStream(input.id) }, async shutdown() { Log.Default.info("worker shutting down") - if (eventStream.abort) eventStream.abort.abort() + + for (const id of [...eventStreams.keys()]) { + stopEventStream(id) + } + await Instance.disposeAll() if (server) await server.stop(true) }, diff --git a/packages/opencode/test/fixture/tui-plugin.ts b/packages/opencode/test/fixture/tui-plugin.ts index a9b1ed4ce8..7ddcc77338 100644 --- a/packages/opencode/test/fixture/tui-plugin.ts +++ b/packages/opencode/test/fixture/tui-plugin.ts @@ -82,8 +82,6 @@ function themeCurrent(): HostPluginApi["theme"]["current"] { type Opts = { client?: HostPluginApi["client"] | (() => HostPluginApi["client"]) - scopedClient?: HostPluginApi["scopedClient"] - workspace?: Partial renderer?: HostPluginApi["renderer"] count?: Count keybind?: Partial @@ -127,11 +125,6 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi { ? () => opts.client as HostPluginApi["client"] : fallback const client = () => read() - const scopedClient = opts.scopedClient ?? ((_workspaceID?: string) => client()) - const workspace: HostPluginApi["workspace"] = { - current: opts.workspace?.current ?? (() => undefined), - set: opts.workspace?.set ?? (() => {}), - } let depth = 0 let size: "medium" | "large" | "xlarge" = "medium" const has = opts.theme?.has ?? (() => false) @@ -171,8 +164,6 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi { get client() { return client() }, - scopedClient, - workspace, event: { on: () => { if (count) count.event_add += 1 diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index 27b59b8d55..8f8439fab4 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -484,8 +484,6 @@ export type TuiPluginApi = { state: TuiState theme: TuiTheme client: OpencodeClient - scopedClient: (workspaceID?: string) => OpencodeClient - workspace: TuiWorkspace event: TuiEventBus renderer: CliRenderer slots: TuiSlots