Skip to content

Commit 42206da

Browse files
authored
refactor(tui): switch to global events and start passing workspace param (#21719)
1 parent 44f3819 commit 42206da

31 files changed

Lines changed: 850 additions & 246 deletions

packages/opencode/src/bus/global.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export const GlobalBus = new EventEmitter<{
44
event: [
55
{
66
directory?: string
7+
project?: string
8+
workspace?: string
79
payload: any
810
},
911
]

packages/opencode/src/bus/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import z from "zod"
22
import { Effect, Exit, Layer, PubSub, Scope, ServiceMap, Stream } from "effect"
33
import { Log } from "../util/log"
4-
import { Instance } from "../project/instance"
54
import { BusEvent } from "./bus-event"
65
import { GlobalBus } from "./global"
6+
import { WorkspaceContext } from "@/control-plane/workspace-context"
77
import { InstanceState } from "@/effect/instance-state"
88
import { makeRuntime } from "@/effect/run-service"
99

@@ -91,8 +91,13 @@ export namespace Bus {
9191
yield* PubSub.publish(s.wildcard, payload)
9292

9393
const dir = yield* InstanceState.directory
94+
const context = yield* InstanceState.context
95+
const workspace = yield* InstanceState.workspaceID
96+
9497
GlobalBus.emit("event", {
9598
directory: dir,
99+
project: context.project.id,
100+
workspace,
96101
payload,
97102
})
98103
})

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
batch,
1515
Show,
1616
on,
17-
onCleanup,
1817
} from "solid-js"
1918
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
2019
import { Flag } from "@/flag/flag"
@@ -23,6 +22,8 @@ import { DialogProvider, useDialog } from "@tui/ui/dialog"
2322
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
2423
import { ErrorComponent } from "@tui/component/error-component"
2524
import { PluginRouteMissing } from "@tui/component/plugin-route-missing"
25+
import { ProjectProvider } from "@tui/context/project"
26+
import { useEvent } from "@tui/context/event"
2627
import { SDKProvider, useSDK } from "@tui/context/sdk"
2728
import { StartupLoading } from "@tui/component/startup-loading"
2829
import { SyncProvider, useSync } from "@tui/context/sync"
@@ -54,7 +55,6 @@ import { KVProvider, useKV } from "./context/kv"
5455
import { Provider } from "@/provider/provider"
5556
import { ArgsProvider, useArgs, type Args } from "./context/args"
5657
import open from "open"
57-
import { writeHeapSnapshot } from "v8"
5858
import { PromptRefProvider, usePromptRef } from "./context/prompt"
5959
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
6060
import { TuiConfig } from "@/config/tui"
@@ -216,27 +216,29 @@ export function tui(input: {
216216
headers={input.headers}
217217
events={input.events}
218218
>
219-
<SyncProvider>
220-
<ThemeProvider mode={mode}>
221-
<LocalProvider>
222-
<KeybindProvider>
223-
<PromptStashProvider>
224-
<DialogProvider>
225-
<CommandProvider>
226-
<FrecencyProvider>
227-
<PromptHistoryProvider>
228-
<PromptRefProvider>
229-
<App onSnapshot={input.onSnapshot} />
230-
</PromptRefProvider>
231-
</PromptHistoryProvider>
232-
</FrecencyProvider>
233-
</CommandProvider>
234-
</DialogProvider>
235-
</PromptStashProvider>
236-
</KeybindProvider>
237-
</LocalProvider>
238-
</ThemeProvider>
239-
</SyncProvider>
219+
<ProjectProvider>
220+
<SyncProvider>
221+
<ThemeProvider mode={mode}>
222+
<LocalProvider>
223+
<KeybindProvider>
224+
<PromptStashProvider>
225+
<DialogProvider>
226+
<CommandProvider>
227+
<FrecencyProvider>
228+
<PromptHistoryProvider>
229+
<PromptRefProvider>
230+
<App onSnapshot={input.onSnapshot} />
231+
</PromptRefProvider>
232+
</PromptHistoryProvider>
233+
</FrecencyProvider>
234+
</CommandProvider>
235+
</DialogProvider>
236+
</PromptStashProvider>
237+
</KeybindProvider>
238+
</LocalProvider>
239+
</ThemeProvider>
240+
</SyncProvider>
241+
</ProjectProvider>
240242
</SDKProvider>
241243
</TuiConfigProvider>
242244
</RouteProvider>
@@ -260,6 +262,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
260262
const kv = useKV()
261263
const command = useCommandDialog()
262264
const keybind = useKeybind()
265+
const event = useEvent()
263266
const sdk = useSDK()
264267
const toast = useToast()
265268
const themeState = useTheme()
@@ -283,6 +286,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
283286
route,
284287
routes,
285288
bump: () => setRouteRev((x) => x + 1),
289+
event,
286290
sdk,
287291
sync,
288292
theme: themeState,
@@ -491,12 +495,9 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
491495
const current = promptRef.current
492496
// Don't require focus - if there's any text, preserve it
493497
const currentPrompt = current?.current?.input ? current.current : undefined
494-
const workspaceID =
495-
route.data.type === "session" ? sync.session.get(route.data.sessionID)?.workspaceID : undefined
496498
route.navigate({
497499
type: "home",
498500
initialPrompt: currentPrompt,
499-
workspaceID,
500501
})
501502
dialog.clear()
502503
},
@@ -806,11 +807,11 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
806807
},
807808
])
808809

809-
sdk.event.on(TuiEvent.CommandExecute.type, (evt) => {
810+
event.on(TuiEvent.CommandExecute.type, (evt) => {
810811
command.trigger(evt.properties.command)
811812
})
812813

813-
sdk.event.on(TuiEvent.ToastShow.type, (evt) => {
814+
event.on(TuiEvent.ToastShow.type, (evt) => {
814815
toast.show({
815816
title: evt.properties.title,
816817
message: evt.properties.message,
@@ -819,14 +820,14 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
819820
})
820821
})
821822

822-
sdk.event.on(TuiEvent.SessionSelect.type, (evt) => {
823+
event.on(TuiEvent.SessionSelect.type, (evt) => {
823824
route.navigate({
824825
type: "session",
825826
sessionID: evt.properties.sessionID,
826827
})
827828
})
828829

829-
sdk.event.on("session.deleted", (evt) => {
830+
event.on("session.deleted", (evt) => {
830831
if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
831832
route.navigate({ type: "home" })
832833
toast.show({
@@ -836,7 +837,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
836837
}
837838
})
838839

839-
sdk.event.on("session.error", (evt) => {
840+
event.on("session.error", (evt) => {
840841
const error = evt.properties.error
841842
if (error && typeof error === "object" && error.name === "MessageAbortedError") return
842843
const message = errorMessage(error)
@@ -848,7 +849,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
848849
})
849850
})
850851

851-
sdk.event.on("installation.update-available", async (evt) => {
852+
event.on("installation.update-available", async (evt) => {
852853
const version = evt.properties.version
853854

854855
const skipped = kv.get("skipped_version")

packages/opencode/src/cli/cmd/tui/component/dialog-workspace-list.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useDialog } from "@tui/ui/dialog"
22
import { DialogSelect } from "@tui/ui/dialog-select"
3+
import { useProject } from "@tui/context/project"
34
import { useRoute } from "@tui/context/route"
45
import { useSync } from "@tui/context/sync"
56
import { createEffect, createMemo, createSignal, onMount } from "solid-js"
@@ -14,7 +15,7 @@ function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>
1415
return createOpencodeClient({
1516
baseUrl: sdk.url,
1617
fetch: sdk.fetch,
17-
directory: sync.data.path.directory || sdk.directory,
18+
directory: sync.path.directory || sdk.directory,
1819
experimental_workspaceID: workspaceID,
1920
})
2021
}
@@ -149,6 +150,7 @@ function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promi
149150

150151
export function DialogWorkspaceList() {
151152
const dialog = useDialog()
153+
const project = useProject()
152154
const route = useRoute()
153155
const sync = useSync()
154156
const sdk = useSDK()
@@ -168,8 +170,9 @@ export function DialogWorkspaceList() {
168170
forceCreate,
169171
})
170172

171-
async function selectWorkspace(workspaceID: string) {
172-
if (workspaceID === "__local__") {
173+
async function selectWorkspace(workspaceID: string | null) {
174+
if (workspaceID == null) {
175+
project.workspace.set(undefined)
173176
if (localCount() > 0) {
174177
dialog.replace(() => <DialogSessionList localOnly={true} />)
175178
return
@@ -199,12 +202,7 @@ export function DialogWorkspaceList() {
199202
await open(workspaceID)
200203
}
201204

202-
const currentWorkspaceID = createMemo(() => {
203-
if (route.data.type === "session") {
204-
return sync.session.get(route.data.sessionID)?.workspaceID ?? "__local__"
205-
}
206-
return "__local__"
207-
})
205+
const currentWorkspaceID = createMemo(() => project.workspace.current())
208206

209207
const localCount = createMemo(
210208
() => sync.data.session.filter((session) => !session.workspaceID && !session.parentID).length,
@@ -234,7 +232,7 @@ export function DialogWorkspaceList() {
234232
const options = createMemo(() => [
235233
{
236234
title: "Local",
237-
value: "__local__",
235+
value: null,
238236
category: "Workspace",
239237
description: "Use the local machine",
240238
footer: `${localCount()} session${localCount() === 1 ? "" : "s"}`,
@@ -292,7 +290,7 @@ export function DialogWorkspaceList() {
292290
keybind: keybind.all.session_delete?.[0],
293291
title: "delete",
294292
onTrigger: async (option) => {
295-
if (option.value === "__create__" || option.value === "__local__") return
293+
if (option.value === "__create__" || option.value === null) return
296294
if (toDelete() !== option.value) {
297295
setToDelete(option.value)
298296
return
@@ -307,6 +305,7 @@ export function DialogWorkspaceList() {
307305
return
308306
}
309307
if (currentWorkspaceID() === option.value) {
308+
project.workspace.set(undefined)
310309
route.navigate({
311310
type: "home",
312311
})

packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ export function Autocomplete(props: {
250250
const width = props.anchor().width - 4
251251
options.push(
252252
...sortedFiles.map((item): AutocompleteOption => {
253-
const baseDir = (sync.data.path.directory || process.cwd()).replace(/\/+$/, "")
253+
const baseDir = (sync.path.directory || process.cwd()).replace(/\/+$/, "")
254254
const fullPath = `${baseDir}/${item}`
255255
const urlObj = pathToFileURL(fullPath)
256256
let filename = item

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { EmptyBorder, SplitBorder } from "@tui/component/border"
1010
import { useSDK } from "@tui/context/sdk"
1111
import { useRoute } from "@tui/context/route"
1212
import { useSync } from "@tui/context/sync"
13+
import { useEvent } from "@tui/context/event"
1314
import { MessageID, PartID } from "@/session/schema"
1415
import { createStore, produce } from "solid-js/store"
1516
import { useKeybind } from "@tui/context/keybind"
@@ -115,8 +116,9 @@ export function Prompt(props: PromptProps) {
115116
const agentStyleId = syntax().getStyleId("extmark.agent")!
116117
const pasteStyleId = syntax().getStyleId("extmark.paste")!
117118
let promptPartTypeId = 0
119+
const event = useEvent()
118120

119-
sdk.event.on(TuiEvent.PromptAppend.type, (evt) => {
121+
event.on(TuiEvent.PromptAppend.type, (evt) => {
120122
if (!input || input.isDestroyed) return
121123
input.insertText(evt.properties.text)
122124
setTimeout(() => {

packages/opencode/src/cli/cmd/tui/context/directory.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { createMemo } from "solid-js"
2+
import { useProject } from "./project"
23
import { useSync } from "./sync"
34
import { Global } from "@/global"
45

56
export function useDirectory() {
7+
const project = useProject()
68
const sync = useSync()
79
return createMemo(() => {
8-
const directory = sync.data.path.directory || process.cwd()
10+
const directory = project.instance.path().directory || process.cwd()
911
const result = directory.replace(Global.Path.home, "~")
1012
if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch
1113
return result
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { Event } from "@opencode-ai/sdk/v2"
2+
import { useProject } from "./project"
3+
import { useSDK } from "./sdk"
4+
5+
export function useEvent() {
6+
const project = useProject()
7+
const sdk = useSDK()
8+
9+
function subscribe(handler: (event: Event) => void) {
10+
return sdk.event.on("event", (event) => {
11+
// Special hack for truly global events
12+
if (event.directory === "global") {
13+
handler(event.payload)
14+
}
15+
16+
if (project.workspace.current()) {
17+
if (event.workspace === project.workspace.current()) {
18+
handler(event.payload)
19+
}
20+
21+
return
22+
}
23+
24+
if (event.directory === project.instance.directory()) {
25+
handler(event.payload)
26+
}
27+
})
28+
}
29+
30+
function on<T extends Event["type"]>(type: T, handler: (event: Extract<Event, { type: T }>) => void) {
31+
return subscribe((event) => {
32+
if (event.type !== type) return
33+
handler(event as Extract<Event, { type: T }>)
34+
})
35+
}
36+
37+
return {
38+
subscribe,
39+
on,
40+
}
41+
}

0 commit comments

Comments
 (0)