Skip to content

Commit c53ae2e

Browse files
Apply PR #21719: refactor(tui): switch to global events and start passing workspace param
2 parents 8adc207 + 36df677 commit c53ae2e

31 files changed

Lines changed: 850 additions & 247 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"
@@ -55,7 +56,6 @@ import { KVProvider, useKV } from "./context/kv"
5556
import { Provider } from "@/provider/provider"
5657
import { ArgsProvider, useArgs, type Args } from "./context/args"
5758
import open from "open"
58-
import { writeHeapSnapshot } from "v8"
5959
import { PromptRefProvider, usePromptRef } from "./context/prompt"
6060
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
6161
import { TuiConfig } from "@/config/tui"
@@ -217,27 +217,29 @@ export function tui(input: {
217217
headers={input.headers}
218218
events={input.events}
219219
>
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>
220+
<ProjectProvider>
221+
<SyncProvider>
222+
<ThemeProvider mode={mode}>
223+
<LocalProvider>
224+
<KeybindProvider>
225+
<PromptStashProvider>
226+
<DialogProvider>
227+
<CommandProvider>
228+
<FrecencyProvider>
229+
<PromptHistoryProvider>
230+
<PromptRefProvider>
231+
<App onSnapshot={input.onSnapshot} />
232+
</PromptRefProvider>
233+
</PromptHistoryProvider>
234+
</FrecencyProvider>
235+
</CommandProvider>
236+
</DialogProvider>
237+
</PromptStashProvider>
238+
</KeybindProvider>
239+
</LocalProvider>
240+
</ThemeProvider>
241+
</SyncProvider>
242+
</ProjectProvider>
241243
</SDKProvider>
242244
</TuiConfigProvider>
243245
</RouteProvider>
@@ -261,6 +263,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
261263
const kv = useKV()
262264
const command = useCommandDialog()
263265
const keybind = useKeybind()
266+
const event = useEvent()
264267
const sdk = useSDK()
265268
const toast = useToast()
266269
const themeState = useTheme()
@@ -284,6 +287,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
284287
route,
285288
routes,
286289
bump: () => setRouteRev((x) => x + 1),
290+
event,
287291
sdk,
288292
sync,
289293
theme: themeState,
@@ -492,12 +496,9 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
492496
const current = promptRef.current
493497
// Don't require focus - if there's any text, preserve it
494498
const currentPrompt = current?.current?.input ? current.current : undefined
495-
const workspaceID =
496-
route.data.type === "session" ? sync.session.get(route.data.sessionID)?.workspaceID : undefined
497499
route.navigate({
498500
type: "home",
499501
initialPrompt: currentPrompt,
500-
workspaceID,
501502
})
502503
dialog.clear()
503504
},
@@ -836,11 +837,11 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
836837
},
837838
])
838839

839-
sdk.event.on(TuiEvent.CommandExecute.type, (evt) => {
840+
event.on(TuiEvent.CommandExecute.type, (evt) => {
840841
command.trigger(evt.properties.command)
841842
})
842843

843-
sdk.event.on(TuiEvent.ToastShow.type, (evt) => {
844+
event.on(TuiEvent.ToastShow.type, (evt) => {
844845
toast.show({
845846
title: evt.properties.title,
846847
message: evt.properties.message,
@@ -849,14 +850,14 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
849850
})
850851
})
851852

852-
sdk.event.on(TuiEvent.SessionSelect.type, (evt) => {
853+
event.on(TuiEvent.SessionSelect.type, (evt) => {
853854
route.navigate({
854855
type: "session",
855856
sessionID: evt.properties.sessionID,
856857
})
857858
})
858859

859-
sdk.event.on("session.deleted", (evt) => {
860+
event.on("session.deleted", (evt) => {
860861
if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
861862
route.navigate({ type: "home" })
862863
toast.show({
@@ -866,7 +867,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
866867
}
867868
})
868869

869-
sdk.event.on("session.error", (evt) => {
870+
event.on("session.error", (evt) => {
870871
const error = evt.properties.error
871872
if (error && typeof error === "object" && error.name === "MessageAbortedError") return
872873
const message = errorMessage(error)
@@ -878,7 +879,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
878879
})
879880
})
880881

881-
sdk.event.on("installation.update-available", async (evt) => {
882+
event.on("installation.update-available", async (evt) => {
882883
const version = evt.properties.version
883884

884885
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"
@@ -116,8 +117,9 @@ export function Prompt(props: PromptProps) {
116117
const agentStyleId = syntax().getStyleId("extmark.agent")!
117118
const pasteStyleId = syntax().getStyleId("extmark.paste")!
118119
let promptPartTypeId = 0
120+
const event = useEvent()
119121

120-
sdk.event.on(TuiEvent.PromptAppend.type, (evt) => {
122+
event.on(TuiEvent.PromptAppend.type, (evt) => {
121123
if (!input || input.isDestroyed) return
122124
input.insertText(evt.properties.text)
123125
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)