refactor(core): support multiple event streams in worker and remove workspaces from plugin api (#21348)

pull/19054/merge
James Long 2026-04-07 13:22:34 -04:00 committed by GitHub
parent ec8b9810b4
commit 5d48e7bd44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 51 additions and 90 deletions

View File

@ -289,9 +289,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
toast,
renderer,
})
onCleanup(() => {
api.dispose()
})
const [ready, setReady] = createSignal(false)
TuiPluginRuntime.init(api)
.catch((error) => {

View File

@ -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,
}
},

View File

@ -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<string | undefined, OpencodeClient>
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<string | undefined, OpencodeClient>()
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()
},
}
}

View File

@ -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,

View File

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

View File

@ -43,9 +43,18 @@ function createWorkerFetch(client: RpcClient): typeof fetch {
function createEventSource(client: RpcClient): EventSource {
return {
on: (handler) => client.on<Event>("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 })
}
},
}
}

View File

@ -45,20 +45,20 @@ GlobalBus.on("event", (event) => {
let server: Awaited<ReturnType<typeof Server.listen>> | undefined
const eventStream = {
abort: undefined as AbortController | undefined,
}
const eventStreams = new Map<string, AbortController>()
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<boolean>((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<string, string>; 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)
},

View File

@ -82,8 +82,6 @@ function themeCurrent(): HostPluginApi["theme"]["current"] {
type Opts = {
client?: HostPluginApi["client"] | (() => HostPluginApi["client"])
scopedClient?: HostPluginApi["scopedClient"]
workspace?: Partial<HostPluginApi["workspace"]>
renderer?: HostPluginApi["renderer"]
count?: Count
keybind?: Partial<HostPluginApi["keybind"]>
@ -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

View File

@ -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