From 42206da1f8f42add18b6a107f5df9b96a3bd59f9 Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 10 Apr 2026 10:47:27 -0400 Subject: refactor(tui): switch to global events and start passing workspace param (#21719) --- packages/sdk/openapi.json | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'packages/sdk/openapi.json') diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 450de5131..40361c280 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -9926,6 +9926,12 @@ "directory": { "type": "string" }, + "project": { + "type": "string" + }, + "workspace": { + "type": "string" + }, "payload": { "$ref": "#/components/schemas/Event" } -- cgit v1.2.3 From 180ded6a27c49c0f95c8af5ff17ccacaa54eceab Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 10 Apr 2026 13:03:20 -0400 Subject: rector(core,tui): handle workspace state in project context, add workspace status, improve ui (#21896) --- packages/opencode/specs/tui-plugins.md | 5 +- packages/opencode/src/cli/cmd/tui/app.tsx | 19 +- .../cli/cmd/tui/component/dialog-session-list.tsx | 80 +++++- .../cmd/tui/component/dialog-workspace-create.tsx | 121 ++++++++ .../cmd/tui/component/dialog-workspace-list.tsx | 319 --------------------- .../component/workspace/dialog-session-list.tsx | 151 ---------- .../opencode/src/cli/cmd/tui/context/project.tsx | 53 +++- packages/opencode/src/cli/cmd/tui/context/sync.tsx | 26 +- packages/opencode/src/cli/cmd/tui/plugin/api.tsx | 8 - .../opencode/src/cli/cmd/tui/ui/dialog-select.tsx | 53 +++- packages/opencode/src/control-plane/workspace.ts | 145 +++++++--- packages/opencode/src/server/router.ts | 22 +- packages/opencode/src/server/routes/workspace.ts | 22 ++ packages/opencode/src/session/index.ts | 17 +- packages/opencode/src/sync/index.ts | 10 +- .../opencode/test/cli/tui/sync-provider.test.tsx | 1 - packages/opencode/test/fixture/tui-plugin.ts | 7 +- packages/opencode/test/session/session.test.ts | 23 ++ packages/plugin/src/tui.ts | 4 - packages/sdk/js/src/v2/gen/sdk.gen.ts | 31 ++ packages/sdk/js/src/v2/gen/types.gen.ts | 34 +++ packages/sdk/openapi.json | 88 ++++++ 22 files changed, 629 insertions(+), 610 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx delete mode 100644 packages/opencode/src/cli/cmd/tui/component/dialog-workspace-list.tsx delete mode 100644 packages/opencode/src/cli/cmd/tui/component/workspace/dialog-session-list.tsx (limited to 'packages/sdk/openapi.json') diff --git a/packages/opencode/specs/tui-plugins.md b/packages/opencode/specs/tui-plugins.md index c5420586e..943125b79 100644 --- a/packages/opencode/specs/tui-plugins.md +++ b/packages/opencode/specs/tui-plugins.md @@ -202,7 +202,7 @@ Top-level API groups exposed to `tui(api, options, meta)`: - `api.kv.get`, `set`, `ready` - `api.state` - `api.theme.current`, `selected`, `has`, `set`, `install`, `mode`, `ready` -- `api.client`, `api.scopedClient(workspaceID?)`, `api.workspace.current()`, `api.workspace.set(workspaceID?)` +- `api.client` - `api.event.on(type, handler)` - `api.renderer` - `api.slots.register(plugin)` @@ -270,7 +270,6 @@ Command behavior: - `provider` - `path.{state,config,worktree,directory}` - `vcs?.branch` - - `workspace.list()` / `workspace.get(workspaceID)` - `session.count()` - `session.diff(sessionID)` - `session.todo(sessionID)` @@ -282,8 +281,6 @@ Command behavior: - `lsp()` - `mcp()` - `api.client` always reflects the current runtime client. -- `api.scopedClient(workspaceID?)` creates or reuses a client bound to a workspace. -- `api.workspace.set(...)` rebinds the active workspace; `api.client` follows that rebind. - `api.event.on(type, handler)` subscribes to the TUI event stream and returns an unsubscribe function. - `api.renderer` exposes the raw `CliRenderer`. diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 2f8d1f7bb..8c4f596fd 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -22,7 +22,7 @@ import { DialogProvider, useDialog } from "@tui/ui/dialog" import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider" import { ErrorComponent } from "@tui/component/error-component" import { PluginRouteMissing } from "@tui/component/plugin-route-missing" -import { ProjectProvider } from "@tui/context/project" +import { ProjectProvider, useProject } from "@tui/context/project" import { useEvent } from "@tui/context/event" import { SDKProvider, useSDK } from "@tui/context/sdk" import { StartupLoading } from "@tui/component/startup-loading" @@ -36,7 +36,6 @@ import { DialogHelp } from "./ui/dialog-help" import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command" import { DialogAgent } from "@tui/component/dialog-agent" import { DialogSessionList } from "@tui/component/dialog-session-list" -import { DialogWorkspaceList } from "@tui/component/dialog-workspace-list" import { DialogConsoleOrg } from "@tui/component/dialog-console-org" import { KeybindProvider, useKeybind } from "@tui/context/keybind" import { ThemeProvider, useTheme } from "@tui/context/theme" @@ -465,22 +464,6 @@ function App(props: { onSnapshot?: () => Promise }) { dialog.replace(() => ) }, }, - ...(Flag.OPENCODE_EXPERIMENTAL_WORKSPACES - ? [ - { - title: "Manage workspaces", - value: "workspace.list", - category: "Workspace", - suggested: true, - slash: { - name: "workspaces", - }, - onSelect: () => { - dialog.replace(() => ) - }, - }, - ] - : []), { title: "New session", suggested: route.data.type === "session", diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 775969bfc..9ecb21e82 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -2,25 +2,31 @@ import { useDialog } from "@tui/ui/dialog" import { DialogSelect } from "@tui/ui/dialog-select" import { useRoute } from "@tui/context/route" import { useSync } from "@tui/context/sync" -import { createMemo, createSignal, createResource, onMount, Show } from "solid-js" +import { createMemo, createResource, createSignal, onMount } from "solid-js" import { Locale } from "@/util/locale" +import { useProject } from "@tui/context/project" import { useKeybind } from "../context/keybind" import { useTheme } from "../context/theme" import { useSDK } from "../context/sdk" +import { Flag } from "@/flag/flag" import { DialogSessionRename } from "./dialog-session-rename" -import { useKV } from "../context/kv" +import { Keybind } from "@/util/keybind" import { createDebouncedSignal } from "../util/signal" +import { useToast } from "../ui/toast" +import { DialogWorkspaceCreate, openWorkspaceSession } from "./dialog-workspace-create" import { Spinner } from "./spinner" +type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error" + export function DialogSessionList() { const dialog = useDialog() const route = useRoute() const sync = useSync() + const project = useProject() const keybind = useKeybind() const { theme } = useTheme() const sdk = useSDK() - const kv = useKV() - + const toast = useToast() const [toDelete, setToDelete] = createSignal() const [search, setSearch] = createDebouncedSignal("", 150) @@ -31,15 +37,68 @@ export function DialogSessionList() { }) const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) - const sessions = createMemo(() => searchResults() ?? sync.data.session) + function createWorkspace() { + dialog.replace(() => ( + + openWorkspaceSession({ + dialog, + route, + sdk, + sync, + toast, + workspaceID, + }) + } + /> + )) + } + const options = createMemo(() => { const today = new Date().toDateString() return sessions() .filter((x) => x.parentID === undefined) .toSorted((a, b) => b.time.updated - a.time.updated) .map((x) => { + const workspace = x.workspaceID ? project.workspace.get(x.workspaceID) : undefined + + let workspaceStatus: WorkspaceStatus | null = null + if (x.workspaceID) { + workspaceStatus = project.workspace.status(x.workspaceID) || "error" + } + + let footer = "" + if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { + if (x.workspaceID) { + let desc = "unknown" + if (workspace) { + desc = `${workspace.type}: ${workspace.name}` + } + + footer = ( + <> + {desc}{" "} + + ■ + + + ) + } + } else { + footer = Locale.time(x.time.updated) + } + const date = new Date(x.time.updated) let category = date.toDateString() if (category === today) { @@ -53,7 +112,7 @@ export function DialogSessionList() { bg: isDeleting ? theme.error : undefined, value: x.id, category, - footer: Locale.time(x.time.updated), + footer, gutter: isWorking ? : undefined, } }) @@ -102,6 +161,15 @@ export function DialogSessionList() { dialog.replace(() => ) }, }, + { + keybind: Keybind.parse("ctrl+w")[0], + title: "new workspace", + side: "right", + disabled: !Flag.OPENCODE_EXPERIMENTAL_WORKSPACES, + onTrigger: () => { + createWorkspace() + }, + }, ]} /> ) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx new file mode 100644 index 000000000..40cc1013e --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -0,0 +1,121 @@ +import { createOpencodeClient } from "@opencode-ai/sdk/v2" +import { useDialog } from "@tui/ui/dialog" +import { DialogSelect } from "@tui/ui/dialog-select" +import { useRoute } from "@tui/context/route" +import { useSync } from "@tui/context/sync" +import { useProject } from "@tui/context/project" +import { createMemo, createSignal, onMount } from "solid-js" +import { setTimeout as sleep } from "node:timers/promises" +import { useSDK } from "../context/sdk" +import { useToast } from "../ui/toast" + +function scoped(sdk: ReturnType, sync: ReturnType, workspaceID: string) { + return createOpencodeClient({ + baseUrl: sdk.url, + fetch: sdk.fetch, + directory: sync.path.directory || sdk.directory, + experimental_workspaceID: workspaceID, + }) +} + +export async function openWorkspaceSession(input: { + dialog: ReturnType + route: ReturnType + sdk: ReturnType + sync: ReturnType + toast: ReturnType + workspaceID: string +}) { + const client = scoped(input.sdk, input.sync, input.workspaceID) + while (true) { + const result = await client.session.create({ workspaceID: input.workspaceID }).catch(() => undefined) + if (!result) { + input.toast.show({ + message: "Failed to create workspace session", + variant: "error", + }) + return + } + if (result.response.status >= 500 && result.response.status < 600) { + await sleep(1000) + continue + } + if (!result.data) { + input.toast.show({ + message: "Failed to create workspace session", + variant: "error", + }) + return + } + input.route.navigate({ + type: "session", + sessionID: result.data.id, + }) + input.dialog.clear() + return + } +} + +export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise | void }) { + const dialog = useDialog() + const sync = useSync() + const project = useProject() + const sdk = useSDK() + const toast = useToast() + const [creating, setCreating] = createSignal() + + onMount(() => { + dialog.setSize("medium") + }) + + const options = createMemo(() => { + const type = creating() + if (type) { + return [ + { + title: `Creating ${type} workspace...`, + value: "creating" as const, + description: "This can take a while for remote environments", + }, + ] + } + return [ + { + title: "Worktree", + value: "worktree" as const, + description: "Create a local git worktree", + }, + ] + }) + + const create = async (type: string) => { + if (creating()) return + setCreating(type) + + const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch(() => undefined) + const workspace = result?.data + if (!workspace) { + setCreating(undefined) + toast.show({ + message: "Failed to create workspace", + variant: "error", + }) + return + } + await project.workspace.sync() + await props.onSelect(workspace.id) + setCreating(undefined) + } + + return ( + { + if (option.value === "creating") return + void create(option.value) + }} + /> + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-list.tsx deleted file mode 100644 index 037cebb72..000000000 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-list.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import { useDialog } from "@tui/ui/dialog" -import { DialogSelect } from "@tui/ui/dialog-select" -import { useProject } from "@tui/context/project" -import { useRoute } from "@tui/context/route" -import { useSync } from "@tui/context/sync" -import { createEffect, createMemo, createSignal, onMount } from "solid-js" -import { createOpencodeClient, type Session } from "@opencode-ai/sdk/v2" -import { useSDK } from "../context/sdk" -import { useToast } from "../ui/toast" -import { useKeybind } from "../context/keybind" -import { DialogSessionList } from "./workspace/dialog-session-list" -import { setTimeout as sleep } from "node:timers/promises" - -function scoped(sdk: ReturnType, sync: ReturnType, workspaceID?: string) { - return createOpencodeClient({ - baseUrl: sdk.url, - fetch: sdk.fetch, - directory: sync.path.directory || sdk.directory, - experimental_workspaceID: workspaceID, - }) -} - -async function openWorkspace(input: { - dialog: ReturnType - route: ReturnType - sdk: ReturnType - sync: ReturnType - toast: ReturnType - workspaceID: string - forceCreate?: boolean -}) { - const cacheSession = (session: Session) => { - input.sync.set( - "session", - [...input.sync.data.session.filter((item) => item.id !== session.id), session].toSorted((a, b) => - a.id.localeCompare(b.id), - ), - ) - } - - const client = scoped(input.sdk, input.sync, input.workspaceID) - const listed = input.forceCreate ? undefined : await client.session.list({ roots: true, limit: 1 }) - const session = listed?.data?.[0] - if (session?.id) { - cacheSession(session) - input.route.navigate({ - type: "session", - sessionID: session.id, - }) - input.dialog.clear() - return - } - let created: Session | undefined - while (!created) { - const result = await client.session.create({ workspaceID: input.workspaceID }).catch(() => undefined) - if (!result) { - input.toast.show({ - message: "Failed to open workspace", - variant: "error", - }) - return - } - if (result.response.status >= 500 && result.response.status < 600) { - await sleep(1000) - continue - } - if (!result.data) { - input.toast.show({ - message: "Failed to open workspace", - variant: "error", - }) - return - } - created = result.data - } - cacheSession(created) - input.route.navigate({ - type: "session", - sessionID: created.id, - }) - input.dialog.clear() -} - -function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise }) { - const dialog = useDialog() - const sync = useSync() - const sdk = useSDK() - const toast = useToast() - const [creating, setCreating] = createSignal() - - onMount(() => { - dialog.setSize("medium") - }) - - const options = createMemo(() => { - const type = creating() - if (type) { - return [ - { - title: `Creating ${type} workspace...`, - value: "creating" as const, - description: "This can take a while for remote environments", - }, - ] - } - return [ - { - title: "Worktree", - value: "worktree" as const, - description: "Create a local git worktree", - }, - ] - }) - - const createWorkspace = async (type: string) => { - if (creating()) return - setCreating(type) - - const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch((err) => { - console.log(err) - return undefined - }) - console.log(JSON.stringify(result, null, 2)) - const workspace = result?.data - if (!workspace) { - setCreating(undefined) - toast.show({ - message: "Failed to create workspace", - variant: "error", - }) - return - } - await sync.workspace.sync() - await props.onSelect(workspace.id) - setCreating(undefined) - } - - return ( - { - if (option.value === "creating") return - void createWorkspace(option.value) - }} - /> - ) -} - -export function DialogWorkspaceList() { - const dialog = useDialog() - const project = useProject() - const route = useRoute() - const sync = useSync() - const sdk = useSDK() - const toast = useToast() - const keybind = useKeybind() - const [toDelete, setToDelete] = createSignal() - const [counts, setCounts] = createSignal>({}) - - const open = (workspaceID: string, forceCreate?: boolean) => - openWorkspace({ - dialog, - route, - sdk, - sync, - toast, - workspaceID, - forceCreate, - }) - - async function selectWorkspace(workspaceID: string | null) { - if (workspaceID == null) { - project.workspace.set(undefined) - if (localCount() > 0) { - dialog.replace(() => ) - return - } - route.navigate({ - type: "home", - }) - dialog.clear() - return - } - const count = counts()[workspaceID] - if (count && count > 0) { - dialog.replace(() => ) - return - } - - if (count === 0) { - await open(workspaceID) - return - } - const client = scoped(sdk, sync, workspaceID) - const listed = await client.session.list({ roots: true, limit: 1 }).catch(() => undefined) - if (listed?.data?.length) { - dialog.replace(() => ) - return - } - await open(workspaceID) - } - - const currentWorkspaceID = createMemo(() => project.workspace.current()) - - const localCount = createMemo( - () => sync.data.session.filter((session) => !session.workspaceID && !session.parentID).length, - ) - - let run = 0 - createEffect(() => { - const workspaces = sync.data.workspaceList - const next = ++run - if (!workspaces.length) { - setCounts({}) - return - } - setCounts(Object.fromEntries(workspaces.map((workspace) => [workspace.id, undefined]))) - void Promise.all( - workspaces.map(async (workspace) => { - const client = scoped(sdk, sync, workspace.id) - const result = await client.session.list({ roots: true }).catch(() => undefined) - return [workspace.id, result ? (result.data?.length ?? 0) : null] as const - }), - ).then((entries) => { - if (run !== next) return - setCounts(Object.fromEntries(entries)) - }) - }) - - const options = createMemo(() => [ - { - title: "Local", - value: null, - category: "Workspace", - description: "Use the local machine", - footer: `${localCount()} session${localCount() === 1 ? "" : "s"}`, - }, - ...sync.data.workspaceList.map((workspace) => { - const count = counts()[workspace.id] - return { - title: - toDelete() === workspace.id - ? `Delete ${workspace.id}? Press ${keybind.print("session_delete")} again` - : workspace.id, - value: workspace.id, - category: workspace.type, - description: workspace.branch ? `Branch ${workspace.branch}` : undefined, - footer: - count === undefined - ? "Loading sessions..." - : count === null - ? "Sessions unavailable" - : `${count} session${count === 1 ? "" : "s"}`, - } - }), - { - title: "+ New workspace", - value: "__create__", - category: "Actions", - description: "Create a new workspace", - }, - ]) - - onMount(() => { - dialog.setSize("large") - void sync.workspace.sync() - }) - - return ( - { - setToDelete(undefined) - }} - onSelect={(option) => { - setToDelete(undefined) - if (option.value === "__create__") { - dialog.replace(() => open(workspaceID, true)} />) - return - } - void selectWorkspace(option.value) - }} - keybind={[ - { - keybind: keybind.all.session_delete?.[0], - title: "delete", - onTrigger: async (option) => { - if (option.value === "__create__" || option.value === null) return - if (toDelete() !== option.value) { - setToDelete(option.value) - return - } - const result = await sdk.client.experimental.workspace.remove({ id: option.value }).catch(() => undefined) - setToDelete(undefined) - if (result?.error) { - toast.show({ - message: "Failed to delete workspace", - variant: "error", - }) - return - } - if (currentWorkspaceID() === option.value) { - project.workspace.set(undefined) - route.navigate({ - type: "home", - }) - } - await sync.workspace.sync() - }, - }, - ]} - /> - ) -} diff --git a/packages/opencode/src/cli/cmd/tui/component/workspace/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/workspace/dialog-session-list.tsx deleted file mode 100644 index 326f094a5..000000000 --- a/packages/opencode/src/cli/cmd/tui/component/workspace/dialog-session-list.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { useDialog } from "@tui/ui/dialog" -import { DialogSelect } from "@tui/ui/dialog-select" -import { useRoute } from "@tui/context/route" -import { useSync } from "@tui/context/sync" -import { createMemo, createSignal, createResource, onMount, Show } from "solid-js" -import { Locale } from "@/util/locale" -import { useKeybind } from "../../context/keybind" -import { useTheme } from "../../context/theme" -import { useSDK } from "../../context/sdk" -import { DialogSessionRename } from "../dialog-session-rename" -import { useKV } from "../../context/kv" -import { createDebouncedSignal } from "../../util/signal" -import { Spinner } from "../spinner" -import { useToast } from "../../ui/toast" - -export function DialogSessionList(props: { workspaceID?: string; localOnly?: boolean } = {}) { - const dialog = useDialog() - const route = useRoute() - const sync = useSync() - const keybind = useKeybind() - const { theme } = useTheme() - const sdk = useSDK() - const kv = useKV() - const toast = useToast() - const [toDelete, setToDelete] = createSignal() - const [search, setSearch] = createDebouncedSignal("", 150) - - const [listed, listedActions] = createResource( - () => props.workspaceID, - async (workspaceID) => { - if (!workspaceID) return undefined - const result = await sdk.client.session.list({ roots: true }) - return result.data ?? [] - }, - ) - - const [searchResults] = createResource(search, async (query) => { - if (!query || props.localOnly) return undefined - const result = await sdk.client.session.list({ - search: query, - limit: 30, - ...(props.workspaceID ? { roots: true } : {}), - }) - return result.data ?? [] - }) - - const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) - - const sessions = createMemo(() => { - if (searchResults()) return searchResults()! - if (props.workspaceID) return listed() ?? [] - if (props.localOnly) return sync.data.session.filter((session) => !session.workspaceID) - return sync.data.session - }) - - const options = createMemo(() => { - const today = new Date().toDateString() - return sessions() - .filter((x) => { - if (x.parentID !== undefined) return false - if (props.workspaceID && listed()) return true - if (props.workspaceID) return x.workspaceID === props.workspaceID - if (props.localOnly) return !x.workspaceID - return true - }) - .toSorted((a, b) => b.time.updated - a.time.updated) - .map((x) => { - const date = new Date(x.time.updated) - let category = date.toDateString() - if (category === today) { - category = "Today" - } - const isDeleting = toDelete() === x.id - const status = sync.data.session_status?.[x.id] - const isWorking = status?.type === "busy" - return { - title: isDeleting ? `Press ${keybind.print("session_delete")} again to confirm` : x.title, - bg: isDeleting ? theme.error : undefined, - value: x.id, - category, - footer: Locale.time(x.time.updated), - gutter: isWorking ? : undefined, - } - }) - }) - - onMount(() => { - dialog.setSize("large") - }) - - return ( - { - setToDelete(undefined) - }} - onSelect={(option) => { - route.navigate({ - type: "session", - sessionID: option.value, - }) - dialog.clear() - }} - keybind={[ - { - keybind: keybind.all.session_delete?.[0], - title: "delete", - onTrigger: async (option) => { - if (toDelete() === option.value) { - const deleted = await sdk.client.session - .delete({ - sessionID: option.value, - }) - .then(() => true) - .catch(() => false) - setToDelete(undefined) - if (!deleted) { - toast.show({ - message: "Failed to delete session", - variant: "error", - }) - return - } - if (props.workspaceID) { - listedActions.mutate((sessions) => sessions?.filter((session) => session.id !== option.value)) - return - } - sync.set( - "session", - sync.data.session.filter((session) => session.id !== option.value), - ) - return - } - setToDelete(option.value) - }, - }, - { - keybind: keybind.all.session_rename?.[0], - title: "rename", - onTrigger: async (option) => { - dialog.replace(() => ) - }, - }, - ]} - /> - ) -} diff --git a/packages/opencode/src/cli/cmd/tui/context/project.tsx b/packages/opencode/src/cli/cmd/tui/context/project.tsx index 522e72401..26e5c075d 100644 --- a/packages/opencode/src/cli/cmd/tui/context/project.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/project.tsx @@ -1,9 +1,11 @@ import { batch } from "solid-js" -import type { Path } from "@opencode-ai/sdk" +import type { Path, Workspace } from "@opencode-ai/sdk/v2" import { createStore, reconcile } from "solid-js/store" import { createSimpleContext } from "./helper" import { useSDK } from "./sdk" +type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error" + export const { use: useProject, provider: ProjectProvider } = createSimpleContext({ name: "Project", init: () => { @@ -14,17 +16,22 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex }, instance: { path: { + home: "", state: "", config: "", worktree: "", directory: sdk.directory ?? "", } satisfies Path, }, - workspace: undefined as string | undefined, + workspace: { + current: undefined as string | undefined, + list: [] as Workspace[], + status: {} as Record, + }, }) async function sync() { - const workspace = store.workspace + const workspace = store.workspace.current const [path, project] = await Promise.all([ sdk.client.path.get({ workspace }), sdk.client.project.current({ workspace }), @@ -36,6 +43,27 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex }) } + async function syncWorkspace() { + const listed = await sdk.client.experimental.workspace.list().catch(() => undefined) + if (!listed?.data) return + const status = await sdk.client.experimental.workspace.status().catch(() => undefined) + const next = Object.fromEntries((status?.data ?? []).map((item) => [item.workspaceID, item.status])) + + batch(() => { + setStore("workspace", "list", reconcile(listed.data)) + setStore("workspace", "status", reconcile(next)) + if (!listed.data.some((item) => item.id === store.workspace.current)) { + setStore("workspace", "current", undefined) + } + }) + } + + sdk.event.on("event", (event) => { + if (event.payload.type === "workspace.status") { + setStore("workspace", "status", event.payload.properties.workspaceID, event.payload.properties.status) + } + }) + return { data: store, project() { @@ -51,13 +79,26 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex }, workspace: { current() { - return store.workspace + return store.workspace.current }, set(next?: string | null) { const workspace = next ?? undefined - if (store.workspace === workspace) return - setStore("workspace", workspace) + if (store.workspace.current === workspace) return + setStore("workspace", "current", workspace) + }, + list() { + return store.workspace.list + }, + get(workspaceID: string) { + return store.workspace.list.find((item) => item.id === workspaceID) + }, + status(workspaceID: string) { + return store.workspace.status[workspaceID] + }, + statuses() { + return store.workspace.status }, + sync: syncWorkspace, }, sync, } diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index bbdc74328..498db99a1 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -17,7 +17,6 @@ import type { ProviderListResponse, ProviderAuthMethod, VcsInfo, - Workspace, } from "@opencode-ai/sdk/v2" import { createStore, produce, reconcile } from "solid-js/store" import { useProject } from "@tui/context/project" @@ -75,7 +74,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ [key: string]: McpResource } formatter: FormatterStatus[] - workspaceList: Workspace[] vcs: VcsInfo | undefined }>({ provider_next: { @@ -103,7 +101,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ mcp: {}, mcp_resource: {}, formatter: [], - workspaceList: [], vcs: undefined, }) @@ -111,16 +108,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const project = useProject() const sdk = useSDK() - async function syncWorkspaces() { - const workspace = project.workspace.current() - const result = await sdk.client.experimental.workspace.list().catch(() => undefined) - if (!result?.data) return - setStore("workspaceList", reconcile(result.data)) - if (!result.data.some((item) => item.id === workspace)) { - project.workspace.set(undefined) - } - } - event.subscribe((event) => { switch (event.type) { case "server.instance.disposed": @@ -368,7 +355,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const workspace = project.workspace.current() const start = Date.now() - 30 * 24 * 60 * 60 * 1000 const sessionListPromise = sdk.client.session - .list({ start: start, workspace }) + .list({ start: start }) .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id))) // blocking - include session.list when continuing a session @@ -443,7 +430,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }), sdk.client.provider.auth({ workspace }).then((x) => setStore("provider_auth", reconcile(x.data ?? {}))), sdk.client.vcs.get({ workspace }).then((x) => setStore("vcs", reconcile(x.data))), - syncWorkspaces(), + project.workspace.sync(), ]).then(() => { setStore("status", "complete") }) @@ -522,15 +509,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ fullSyncedSessions.add(sessionID) }, }, - workspace: { - list() { - return store.workspaceList - }, - get(workspaceID: string) { - return store.workspaceList.find((item) => item.id === workspaceID) - }, - sync: syncWorkspaces, - }, bootstrap, } return result diff --git a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx index e43a9cc37..42bf78adb 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx +++ b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx @@ -146,14 +146,6 @@ function stateApi(sync: ReturnType): TuiPluginApi["state"] { branch: sync.data.vcs.branch, } }, - workspace: { - list() { - return sync.data.workspaceList - }, - get(workspaceID) { - return sync.workspace.get(workspaceID) - }, - }, session: { count() { return sync.data.session.length diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index 46821ccce..109b5f2f1 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -26,6 +26,7 @@ export interface DialogSelectProps { keybind?: { keybind?: Keybind.Info title: string + side?: "left" | "right" disabled?: boolean onTrigger: (option: DialogSelectOption) => void }[] @@ -42,6 +43,7 @@ export interface DialogSelectOption { disabled?: boolean bg?: RGBA gutter?: JSX.Element + margin?: JSX.Element onSelect?: (ctx: DialogContext) => void } @@ -234,6 +236,8 @@ export function DialogSelect(props: DialogSelectProps) { props.ref?.(ref) const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled && x.keybind) ?? []) + const left = createMemo(() => keybinds().filter((item) => item.side !== "right")) + const right = createMemo(() => keybinds().filter((item) => item.side === "right")) return ( @@ -312,6 +316,7 @@ export function DialogSelect(props: DialogSelectProps) { { setStore("input", "mouse") }} @@ -335,6 +340,11 @@ export function DialogSelect(props: DialogSelectProps) { paddingRight={3} gap={1} > + + + {option.margin} + + diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index a030d0b6c..bbf79620c 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -5,7 +5,9 @@ import { Database, eq } from "@/storage/db" import { Project } from "@/project/project" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" +import { SyncEvent } from "@/sync" import { Log } from "@/util/log" +import { Filesystem } from "@/util/filesystem" import { ProjectID } from "@/project/schema" import { WorkspaceTable } from "./workspace.sql" import { getAdaptor } from "./adaptors" @@ -14,6 +16,18 @@ import { WorkspaceID } from "./schema" import { parseSSE } from "./sse" export namespace Workspace { + export const Info = WorkspaceInfo.meta({ + ref: "Workspace", + }) + export type Info = z.infer + + export const ConnectionStatus = z.object({ + workspaceID: WorkspaceID.zod, + status: z.enum(["connected", "connecting", "disconnected", "error"]), + error: z.string().optional(), + }) + export type ConnectionStatus = z.infer + export const Event = { Ready: BusEvent.define( "workspace.ready", @@ -27,13 +41,9 @@ export namespace Workspace { message: z.string(), }), ), + Status: BusEvent.define("workspace.status", ConnectionStatus), } - export const Info = WorkspaceInfo.meta({ - ref: "Workspace", - }) - export type Info = z.infer - function fromRow(row: typeof WorkspaceTable.$inferSelect): Info { return { id: row.id, @@ -85,6 +95,9 @@ export namespace Workspace { }) await adaptor.create(config) + + startSync(info) + return info }) @@ -92,18 +105,24 @@ export namespace Workspace { const rows = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(), ) - return rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id)) + const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id)) + for (const space of spaces) startSync(space) + return spaces } export const get = fn(WorkspaceID.zod, async (id) => { const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get()) if (!row) return - return fromRow(row) + const space = fromRow(row) + startSync(space) + return space }) export const remove = fn(WorkspaceID.zod, async (id) => { const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get()) if (row) { + stopSync(id) + const info = fromRow(row) const adaptor = await getAdaptor(row.type) adaptor.remove(info) @@ -111,58 +130,100 @@ export namespace Workspace { return info } }) + + const connections = new Map() + const aborts = new Map() + + function setStatus(id: WorkspaceID, status: ConnectionStatus["status"], error?: string) { + const prev = connections.get(id) + if (prev?.status === status && prev?.error === error) return + const next = { workspaceID: id, status, error } + connections.set(id, next) + GlobalBus.emit("event", { + directory: "global", + workspace: id, + payload: { + type: Event.Status.type, + properties: next, + }, + }) + } + + export function status(): ConnectionStatus[] { + return [...connections.values()] + } + const log = Log.create({ service: "workspace-sync" }) - async function workspaceEventLoop(space: Info, stop: AbortSignal) { - while (!stop.aborted) { - const adaptor = await getAdaptor(space.type) - const target = await Promise.resolve(adaptor.target(space)) + async function workspaceEventLoop(space: Info, signal: AbortSignal) { + log.info("starting sync: " + space.id) - if (target.type === "local") { - return - } + while (!signal.aborted) { + log.info("connecting to sync: " + space.id) - const baseURL = String(target.url).replace(/\/?$/, "/") + setStatus(space.id, "connecting") + const adaptor = await getAdaptor(space.type) + const target = await adaptor.target(space) + + if (target.type === "local") return - const res = await fetch(new URL(baseURL + "/event"), { - method: "GET", - signal: stop, + const res = await fetch(target.url + "/sync/event", { method: "GET", signal }).catch((err: unknown) => { + setStatus(space.id, "error", String(err)) + return undefined }) + if (!res || !res.ok || !res.body) { + log.info("failed to connect to sync: " + res?.status) - if (!res.ok || !res.body) { + setStatus(space.id, "error", res ? `HTTP ${res.status}` : "no response") await sleep(1000) continue } - - // await parseSSE(res.body, stop, (event) => { - // GlobalBus.emit("event", { - // directory: space.id, - // payload: event, - // }) - // }) - - // Wait 250ms and retry if SSE connection fails + setStatus(space.id, "connected") + await parseSSE(res.body, signal, (evt) => { + const event = evt as SyncEvent.SerializedEvent + + try { + if (!event.type.startsWith("server.")) { + SyncEvent.replay(event) + } + } catch (err) { + log.warn("failed to replay sync event", { + workspaceID: space.id, + error: err, + }) + } + }) + setStatus(space.id, "disconnected") + log.info("disconnected to sync: " + space.id) await sleep(250) } } - export function startSyncing(project: Project.Info) { - const stop = new AbortController() - const spaces = list(project).filter((space) => space.type !== "worktree") + function startSync(space: Info) { + if (space.type === "worktree") { + void Filesystem.exists(space.directory!).then((exists) => { + setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist") + }) + return + } - spaces.forEach((space) => { - void workspaceEventLoop(space, stop.signal).catch((error) => { - log.warn("workspace sync listener failed", { - workspaceID: space.id, - error, - }) + if (aborts.has(space.id)) return + const abort = new AbortController() + aborts.set(space.id, abort) + setStatus(space.id, "disconnected") + + void workspaceEventLoop(space, abort.signal).catch((error) => { + setStatus(space.id, "error", String(error)) + log.warn("workspace sync listener failed", { + workspaceID: space.id, + error, }) }) + } - return { - async stop() { - stop.abort() - }, - } + function stopSync(id: WorkspaceID) { + aborts.get(id)?.abort() + aborts.delete(id) + connections.delete(id) } } diff --git a/packages/opencode/src/server/router.ts b/packages/opencode/src/server/router.ts index dcc7924c6..f97724c2e 100644 --- a/packages/opencode/src/server/router.ts +++ b/packages/opencode/src/server/router.ts @@ -29,13 +29,20 @@ function local(method: string, path: string) { return false } -async function getSessionWorkspace(url: URL) { +function getSessionID(url: URL) { if (url.pathname === "/session/status") return null const id = url.pathname.match(/^\/session\/([^/]+)(?:\/|$)/)?.[1] if (!id) return null - const session = await Session.get(SessionID.make(id)).catch(() => undefined) + return SessionID.make(id) +} + +async function getSessionWorkspace(url: URL) { + const id = getSessionID(url) + if (!id) return null + + const session = await Session.get(id).catch(() => undefined) return session?.workspaceID } @@ -71,7 +78,18 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware } const workspace = await Workspace.get(WorkspaceID.make(workspaceID)) + if (!workspace) { + // Special-case deleting a session in case user's data in a + // weird state. Allow them to forcefully delete a synced session + // even if the remote workspace is not in their data. + // + // The lets the `DELETE /session/:id` endpoint through and we've + // made sure that it will run without an instance + if (url.pathname.match(/\/session\/[^/]+$/) && c.req.method === "DELETE") { + return routes().fetch(c.req.raw, c.env) + } + return new Response(`Workspace not found: ${workspaceID}`, { status: 500, headers: { diff --git a/packages/opencode/src/server/routes/workspace.ts b/packages/opencode/src/server/routes/workspace.ts index cd2d844ae..419321654 100644 --- a/packages/opencode/src/server/routes/workspace.ts +++ b/packages/opencode/src/server/routes/workspace.ts @@ -62,6 +62,28 @@ export const WorkspaceRoutes = lazy(() => return c.json(Workspace.list(Instance.project)) }, ) + .get( + "/status", + describeRoute({ + summary: "Workspace status", + description: "Get connection status for workspaces in the current project.", + operationId: "experimental.workspace.status", + responses: { + 200: { + description: "Workspace status", + content: { + "application/json": { + schema: resolver(z.array(Workspace.ConnectionStatus)), + }, + }, + }, + }, + }), + async (c) => { + const ids = new Set(Workspace.list(Instance.project).map((item) => item.id)) + return c.json(Workspace.status().filter((item) => ids.has(item.workspaceID))) + }, + ) .delete( "/:id", describeRoute({ diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index bbd6693c5..e57807e31 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -413,26 +413,35 @@ export namespace Session { }) const children = Effect.fn("Session.children")(function* (parentID: SessionID) { - const ctx = yield* InstanceState.context const rows = yield* db((d) => d .select() .from(SessionTable) - .where(and(eq(SessionTable.project_id, ctx.project.id), eq(SessionTable.parent_id, parentID))) + .where(and(eq(SessionTable.parent_id, parentID))) .all(), ) return rows.map(fromRow) }) - const remove: (sessionID: SessionID) => Effect.Effect = Effect.fnUntraced(function* (sessionID: SessionID) { + const remove: Interface["remove"] = Effect.fnUntraced(function* (sessionID: SessionID) { try { const session = yield* get(sessionID) const kids = yield* children(sessionID) for (const child of kids) { yield* remove(child.id) } + + // `remove` needs to work in all cases, such as a broken + // sessions that run cleanup. In certain cases these will + // run without any instance state, so we need to turn off + // publishing of events in that case + const hasInstance = yield* InstanceState.directory.pipe( + Effect.as(true), + Effect.catchCause(() => Effect.succeed(false)), + ) + yield* Effect.sync(() => { - SyncEvent.run(Event.Deleted, { sessionID, info: session }) + SyncEvent.run(Event.Deleted, { sessionID, info: session }, { publish: hasInstance }) SyncEvent.remove(sessionID) }) } catch (e) { diff --git a/packages/opencode/src/sync/index.ts b/packages/opencode/src/sync/index.ts index 270950fd4..a40939191 100644 --- a/packages/opencode/src/sync/index.ts +++ b/packages/opencode/src/sync/index.ts @@ -165,7 +165,7 @@ export namespace SyncEvent { // and it validets all the sequence ids // * when loading events from db, apply zod validation to ensure shape - export function replay(event: SerializedEvent, options?: { republish: boolean }) { + export function replay(event: SerializedEvent, options?: { publish: boolean }) { const def = registry.get(event.type) if (!def) { throw new Error(`Unknown event type: ${event.type}`) @@ -189,10 +189,10 @@ export namespace SyncEvent { throw new Error(`Sequence mismatch for aggregate "${event.aggregateID}": expected ${expected}, got ${event.seq}`) } - process(def, event, { publish: !!options?.republish }) + process(def, event, { publish: !!options?.publish }) } - export function run(def: Def, data: Event["data"]) { + export function run(def: Def, data: Event["data"], options?: { publish?: boolean }) { const agg = (data as Record)[def.aggregate] // This should never happen: we've enforced it via typescript in // the definition @@ -204,6 +204,8 @@ export namespace SyncEvent { throw new Error(`SyncEvent.run: running old versions of events is not allowed: ${def.type}`) } + const { publish = true } = options || {} + // Note that this is an "immediate" transaction which is critical. // We need to make sure we can safely read and write with nothing // else changing the data from under us @@ -218,7 +220,7 @@ export namespace SyncEvent { const seq = row?.seq != null ? row.seq + 1 : 0 const event = { id, seq, aggregateID: agg, data } - process(def, event, { publish: true }) + process(def, event, { publish }) }, { behavior: "immediate", diff --git a/packages/opencode/test/cli/tui/sync-provider.test.tsx b/packages/opencode/test/cli/tui/sync-provider.test.tsx index ec686b368..3ef126ef4 100644 --- a/packages/opencode/test/cli/tui/sync-provider.test.tsx +++ b/packages/opencode/test/cli/tui/sync-provider.test.tsx @@ -244,7 +244,6 @@ describe("SyncProvider", () => { expect(log.some((item) => item.path === "/path" && item.workspace === "ws_a")).toBe(true) expect(log.some((item) => item.path === "/config" && item.workspace === "ws_a")).toBe(true) - expect(log.some((item) => item.path === "/session" && item.workspace === "ws_a")).toBe(true) expect(log.some((item) => item.path === "/command" && item.workspace === "ws_a")).toBe(true) } finally { app.renderer.destroy() diff --git a/packages/opencode/test/fixture/tui-plugin.ts b/packages/opencode/test/fixture/tui-plugin.ts index 7ddcc7733..26913222e 100644 --- a/packages/opencode/test/fixture/tui-plugin.ts +++ b/packages/opencode/test/fixture/tui-plugin.ts @@ -93,7 +93,6 @@ type Opts = { provider?: HostPluginApi["state"]["provider"] path?: HostPluginApi["state"]["path"] vcs?: HostPluginApi["state"]["vcs"] - workspace?: Partial session?: Partial part?: HostPluginApi["state"]["part"] lsp?: HostPluginApi["state"]["lsp"] @@ -277,15 +276,11 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi { return opts.state?.provider ?? [] }, get path() { - return opts.state?.path ?? { state: "", config: "", worktree: "", directory: "" } + return opts.state?.path ?? { home: "", state: "", config: "", worktree: "", directory: "" } }, get vcs() { return opts.state?.vcs }, - workspace: { - list: opts.state?.workspace?.list ?? (() => []), - get: opts.state?.workspace?.get ?? (() => undefined), - }, session: { count: opts.state?.session?.count ?? (() => 0), diff: opts.state?.session?.diff ?? (() => []), diff --git a/packages/opencode/test/session/session.test.ts b/packages/opencode/test/session/session.test.ts index 0c18f92ba..75c74002a 100644 --- a/packages/opencode/test/session/session.test.ts +++ b/packages/opencode/test/session/session.test.ts @@ -6,6 +6,7 @@ import { Log } from "../../src/util/log" import { Instance } from "../../src/project/instance" import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID } from "../../src/session/schema" +import { tmpdir } from "../fixture/fixture" const projectRoot = path.join(__dirname, "../..") Log.init({ print: false }) @@ -140,3 +141,25 @@ describe("step-finish token propagation via Bus event", () => { { timeout: 30000 }, ) }) + +describe("Session", () => { + test("remove works without an instance", async () => { + await using tmp = await tmpdir({ git: true }) + + const session = await Instance.provide({ + directory: tmp.path, + fn: async () => Session.create({ title: "remove-without-instance" }), + }) + + await expect(async () => { + await Session.remove(session.id) + }).not.toThrow() + + let missing = false + await Session.get(session.id).catch(() => { + missing = true + }) + + expect(missing).toBe(true) + }) +}) diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index 8f8439fab..e6f832f7e 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -272,10 +272,6 @@ export type TuiState = { directory: string } readonly vcs: { branch?: string } | undefined - readonly workspace: { - list: () => ReadonlyArray - get: (workspaceID: string) => Workspace | undefined - } session: { count: () => number diff: (sessionID: string) => ReadonlyArray diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index b2e37db59..d06a504d6 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -34,6 +34,7 @@ import type { ExperimentalWorkspaceListResponses, ExperimentalWorkspaceRemoveErrors, ExperimentalWorkspaceRemoveResponses, + ExperimentalWorkspaceStatusResponses, FileListResponses, FilePartInput, FilePartSource, @@ -1163,6 +1164,36 @@ export class Workspace extends HeyApiClient { }) } + /** + * Workspace status + * + * Get connection status for workspaces in the current project. + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/workspace/status", + ...options, + ...params, + }) + } + /** * Remove workspace * diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 823c452f9..c1a77bfe8 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -330,6 +330,15 @@ export type EventWorkspaceFailed = { } } +export type EventWorkspaceStatus = { + type: "workspace.status" + properties: { + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" + error?: string + } +} + export type QuestionOption = { /** * Display text (1-5 words, concise) @@ -988,6 +997,7 @@ export type Event = | EventCommandExecuted | EventWorkspaceReady | EventWorkspaceFailed + | EventWorkspaceStatus | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected @@ -2857,6 +2867,30 @@ export type ExperimentalWorkspaceCreateResponses = { export type ExperimentalWorkspaceCreateResponse = ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses] +export type ExperimentalWorkspaceStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/status" +} + +export type ExperimentalWorkspaceStatusResponses = { + /** + * Workspace status + */ + 200: Array<{ + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" + error?: string + }> +} + +export type ExperimentalWorkspaceStatusResponse = + ExperimentalWorkspaceStatusResponses[keyof ExperimentalWorkspaceStatusResponses] + export type ExperimentalWorkspaceRemoveData = { body?: never path: { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 40361c280..deece485e 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -1656,6 +1656,64 @@ ] } }, + "/experimental/workspace/status": { + "get": { + "operationId": "experimental.workspace.status", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "Workspace status", + "description": "Get connection status for workspaces in the current project.", + "responses": { + "200": { + "description": "Workspace status", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "workspaceID": { + "type": "string", + "pattern": "^wrk.*" + }, + "status": { + "type": "string", + "enum": ["connected", "connecting", "disconnected", "error"] + }, + "error": { + "type": "string" + } + }, + "required": ["workspaceID", "status"] + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.status({\n ...\n})" + } + ] + } + }, "/experimental/workspace/{id}": { "delete": { "operationId": "experimental.workspace.remove", @@ -7966,6 +8024,33 @@ }, "required": ["type", "properties"] }, + "Event.workspace.status": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "workspace.status" + }, + "properties": { + "type": "object", + "properties": { + "workspaceID": { + "type": "string", + "pattern": "^wrk.*" + }, + "status": { + "type": "string", + "enum": ["connected", "connecting", "disconnected", "error"] + }, + "error": { + "type": "string" + } + }, + "required": ["workspaceID", "status"] + } + }, + "required": ["type", "properties"] + }, "QuestionOption": { "type": "object", "properties": { @@ -9858,6 +9943,9 @@ { "$ref": "#/components/schemas/Event.workspace.failed" }, + { + "$ref": "#/components/schemas/Event.workspace.status" + }, { "$ref": "#/components/schemas/Event.question.asked" }, -- cgit v1.2.3 From 57f939767729a7cc6d127674528732e0d9beb24e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 11 Apr 2026 01:48:25 +0000 Subject: chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 34 ++++++------ packages/sdk/openapi.json | 94 ++++++++++++++++----------------- 2 files changed, 64 insertions(+), 64 deletions(-) (limited to 'packages/sdk/openapi.json') diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index c1a77bfe8..7ca6ea05c 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -469,6 +469,21 @@ export type EventTodoUpdated = { } } +export type EventWorktreeReady = { + type: "worktree.ready" + properties: { + name: string + branch: string + } +} + +export type EventWorktreeFailed = { + type: "worktree.failed" + properties: { + message: string + } +} + export type Pty = { id: string title: string @@ -508,21 +523,6 @@ export type EventPtyDeleted = { } } -export type EventWorktreeReady = { - type: "worktree.ready" - properties: { - name: string - branch: string - } -} - -export type EventWorktreeFailed = { - type: "worktree.failed" - properties: { - message: string - } -} - export type OutputFormatText = { type: "text" } @@ -1005,12 +1005,12 @@ export type Event = | EventSessionIdle | EventSessionCompacted | EventTodoUpdated + | EventWorktreeReady + | EventWorktreeFailed | EventPtyCreated | EventPtyUpdated | EventPtyExited | EventPtyDeleted - | EventWorktreeReady - | EventWorktreeFailed | EventMessageUpdated | EventMessageRemoved | EventMessagePartUpdated diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index deece485e..7bc6392b9 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8351,6 +8351,47 @@ }, "required": ["type", "properties"] }, + "Event.worktree.ready": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "worktree.ready" + }, + "properties": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "branch": { + "type": "string" + } + }, + "required": ["name", "branch"] + } + }, + "required": ["type", "properties"] + }, + "Event.worktree.failed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "worktree.failed" + }, + "properties": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + }, + "required": ["type", "properties"] + }, "Pty": { "type": "object", "properties": { @@ -8464,47 +8505,6 @@ }, "required": ["type", "properties"] }, - "Event.worktree.ready": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "worktree.ready" - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "branch": { - "type": "string" - } - }, - "required": ["name", "branch"] - } - }, - "required": ["type", "properties"] - }, - "Event.worktree.failed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "worktree.failed" - }, - "properties": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"] - } - }, - "required": ["type", "properties"] - }, "OutputFormatText": { "type": "object", "properties": { @@ -9968,22 +9968,22 @@ "$ref": "#/components/schemas/Event.todo.updated" }, { - "$ref": "#/components/schemas/Event.pty.created" + "$ref": "#/components/schemas/Event.worktree.ready" }, { - "$ref": "#/components/schemas/Event.pty.updated" + "$ref": "#/components/schemas/Event.worktree.failed" }, { - "$ref": "#/components/schemas/Event.pty.exited" + "$ref": "#/components/schemas/Event.pty.created" }, { - "$ref": "#/components/schemas/Event.pty.deleted" + "$ref": "#/components/schemas/Event.pty.updated" }, { - "$ref": "#/components/schemas/Event.worktree.ready" + "$ref": "#/components/schemas/Event.pty.exited" }, { - "$ref": "#/components/schemas/Event.worktree.failed" + "$ref": "#/components/schemas/Event.pty.deleted" }, { "$ref": "#/components/schemas/Event.message.updated" -- cgit v1.2.3 From b898c6d0ea2fcd83b6c23da52f811c082b329771 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 11 Apr 2026 02:04:02 +0000 Subject: chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 100 ++++++------- packages/sdk/openapi.json | 242 ++++++++++++++++---------------- 2 files changed, 171 insertions(+), 171 deletions(-) (limited to 'packages/sdk/openapi.json') diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 7ca6ea05c..ab0707787 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -316,29 +316,6 @@ export type EventCommandExecuted = { } } -export type EventWorkspaceReady = { - type: "workspace.ready" - properties: { - name: string - } -} - -export type EventWorkspaceFailed = { - type: "workspace.failed" - properties: { - message: string - } -} - -export type EventWorkspaceStatus = { - type: "workspace.status" - properties: { - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" - error?: string - } -} - export type QuestionOption = { /** * Display text (1-5 words, concise) @@ -410,6 +387,29 @@ export type EventQuestionRejected = { } } +export type Todo = { + /** + * Brief description of the task + */ + content: string + /** + * Current status of the task: pending, in_progress, completed, cancelled + */ + status: string + /** + * Priority level of the task: high, medium, low + */ + priority: string +} + +export type EventTodoUpdated = { + type: "todo.updated" + properties: { + sessionID: string + todos: Array + } +} + export type SessionStatus = | { type: "idle" @@ -446,29 +446,6 @@ export type EventSessionCompacted = { } } -export type Todo = { - /** - * Brief description of the task - */ - content: string - /** - * Current status of the task: pending, in_progress, completed, cancelled - */ - status: string - /** - * Priority level of the task: high, medium, low - */ - priority: string -} - -export type EventTodoUpdated = { - type: "todo.updated" - properties: { - sessionID: string - todos: Array - } -} - export type EventWorktreeReady = { type: "worktree.ready" properties: { @@ -523,6 +500,29 @@ export type EventPtyDeleted = { } } +export type EventWorkspaceReady = { + type: "workspace.ready" + properties: { + name: string + } +} + +export type EventWorkspaceFailed = { + type: "workspace.failed" + properties: { + message: string + } +} + +export type EventWorkspaceStatus = { + type: "workspace.status" + properties: { + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" + error?: string + } +} + export type OutputFormatText = { type: "text" } @@ -995,22 +995,22 @@ export type Event = | EventMcpToolsChanged | EventMcpBrowserOpenFailed | EventCommandExecuted - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceStatus | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected + | EventTodoUpdated | EventSessionStatus | EventSessionIdle | EventSessionCompacted - | EventTodoUpdated | EventWorktreeReady | EventWorktreeFailed | EventPtyCreated | EventPtyUpdated | EventPtyExited | EventPtyDeleted + | EventWorkspaceReady + | EventWorkspaceFailed + | EventWorkspaceStatus | EventMessageUpdated | EventMessageRemoved | EventMessagePartUpdated diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 7bc6392b9..71b17966e 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7986,71 +7986,6 @@ }, "required": ["type", "properties"] }, - "Event.workspace.ready": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "workspace.ready" - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": ["name"] - } - }, - "required": ["type", "properties"] - }, - "Event.workspace.failed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "workspace.failed" - }, - "properties": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"] - } - }, - "required": ["type", "properties"] - }, - "Event.workspace.status": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "workspace.status" - }, - "properties": { - "type": "object", - "properties": { - "workspaceID": { - "type": "string", - "pattern": "^wrk.*" - }, - "status": { - "type": "string", - "enum": ["connected", "connecting", "disconnected", "error"] - }, - "error": { - "type": "string" - } - }, - "required": ["workspaceID", "status"] - } - }, - "required": ["type", "properties"] - }, "QuestionOption": { "type": "object", "properties": { @@ -8201,6 +8136,50 @@ }, "required": ["type", "properties"] }, + "Todo": { + "type": "object", + "properties": { + "content": { + "description": "Brief description of the task", + "type": "string" + }, + "status": { + "description": "Current status of the task: pending, in_progress, completed, cancelled", + "type": "string" + }, + "priority": { + "description": "Priority level of the task: high, medium, low", + "type": "string" + } + }, + "required": ["content", "status", "priority"] + }, + "Event.todo.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "todo.updated" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses.*" + }, + "todos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": ["sessionID", "todos"] + } + }, + "required": ["type", "properties"] + }, "SessionStatus": { "anyOf": [ { @@ -8307,50 +8286,6 @@ }, "required": ["type", "properties"] }, - "Todo": { - "type": "object", - "properties": { - "content": { - "description": "Brief description of the task", - "type": "string" - }, - "status": { - "description": "Current status of the task: pending, in_progress, completed, cancelled", - "type": "string" - }, - "priority": { - "description": "Priority level of the task: high, medium, low", - "type": "string" - } - }, - "required": ["content", "status", "priority"] - }, - "Event.todo.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "todo.updated" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses.*" - }, - "todos": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Todo" - } - } - }, - "required": ["sessionID", "todos"] - } - }, - "required": ["type", "properties"] - }, "Event.worktree.ready": { "type": "object", "properties": { @@ -8505,6 +8440,71 @@ }, "required": ["type", "properties"] }, + "Event.workspace.ready": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "workspace.ready" + }, + "properties": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"] + } + }, + "required": ["type", "properties"] + }, + "Event.workspace.failed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "workspace.failed" + }, + "properties": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + }, + "required": ["type", "properties"] + }, + "Event.workspace.status": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "workspace.status" + }, + "properties": { + "type": "object", + "properties": { + "workspaceID": { + "type": "string", + "pattern": "^wrk.*" + }, + "status": { + "type": "string", + "enum": ["connected", "connecting", "disconnected", "error"] + }, + "error": { + "type": "string" + } + }, + "required": ["workspaceID", "status"] + } + }, + "required": ["type", "properties"] + }, "OutputFormatText": { "type": "object", "properties": { @@ -9937,15 +9937,6 @@ { "$ref": "#/components/schemas/Event.command.executed" }, - { - "$ref": "#/components/schemas/Event.workspace.ready" - }, - { - "$ref": "#/components/schemas/Event.workspace.failed" - }, - { - "$ref": "#/components/schemas/Event.workspace.status" - }, { "$ref": "#/components/schemas/Event.question.asked" }, @@ -9955,6 +9946,9 @@ { "$ref": "#/components/schemas/Event.question.rejected" }, + { + "$ref": "#/components/schemas/Event.todo.updated" + }, { "$ref": "#/components/schemas/Event.session.status" }, @@ -9964,9 +9958,6 @@ { "$ref": "#/components/schemas/Event.session.compacted" }, - { - "$ref": "#/components/schemas/Event.todo.updated" - }, { "$ref": "#/components/schemas/Event.worktree.ready" }, @@ -9985,6 +9976,15 @@ { "$ref": "#/components/schemas/Event.pty.deleted" }, + { + "$ref": "#/components/schemas/Event.workspace.ready" + }, + { + "$ref": "#/components/schemas/Event.workspace.failed" + }, + { + "$ref": "#/components/schemas/Event.workspace.status" + }, { "$ref": "#/components/schemas/Event.message.updated" }, -- cgit v1.2.3 From 2e340d976f1d06e803c743e32e0c950f9ba643fb Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 11 Apr 2026 03:53:48 +0000 Subject: chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 100 ++++++------- packages/sdk/openapi.json | 242 ++++++++++++++++---------------- 2 files changed, 171 insertions(+), 171 deletions(-) (limited to 'packages/sdk/openapi.json') diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index ab0707787..7ca6ea05c 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -316,6 +316,29 @@ export type EventCommandExecuted = { } } +export type EventWorkspaceReady = { + type: "workspace.ready" + properties: { + name: string + } +} + +export type EventWorkspaceFailed = { + type: "workspace.failed" + properties: { + message: string + } +} + +export type EventWorkspaceStatus = { + type: "workspace.status" + properties: { + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" + error?: string + } +} + export type QuestionOption = { /** * Display text (1-5 words, concise) @@ -387,29 +410,6 @@ export type EventQuestionRejected = { } } -export type Todo = { - /** - * Brief description of the task - */ - content: string - /** - * Current status of the task: pending, in_progress, completed, cancelled - */ - status: string - /** - * Priority level of the task: high, medium, low - */ - priority: string -} - -export type EventTodoUpdated = { - type: "todo.updated" - properties: { - sessionID: string - todos: Array - } -} - export type SessionStatus = | { type: "idle" @@ -446,6 +446,29 @@ export type EventSessionCompacted = { } } +export type Todo = { + /** + * Brief description of the task + */ + content: string + /** + * Current status of the task: pending, in_progress, completed, cancelled + */ + status: string + /** + * Priority level of the task: high, medium, low + */ + priority: string +} + +export type EventTodoUpdated = { + type: "todo.updated" + properties: { + sessionID: string + todos: Array + } +} + export type EventWorktreeReady = { type: "worktree.ready" properties: { @@ -500,29 +523,6 @@ export type EventPtyDeleted = { } } -export type EventWorkspaceReady = { - type: "workspace.ready" - properties: { - name: string - } -} - -export type EventWorkspaceFailed = { - type: "workspace.failed" - properties: { - message: string - } -} - -export type EventWorkspaceStatus = { - type: "workspace.status" - properties: { - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" - error?: string - } -} - export type OutputFormatText = { type: "text" } @@ -995,22 +995,22 @@ export type Event = | EventMcpToolsChanged | EventMcpBrowserOpenFailed | EventCommandExecuted + | EventWorkspaceReady + | EventWorkspaceFailed + | EventWorkspaceStatus | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected - | EventTodoUpdated | EventSessionStatus | EventSessionIdle | EventSessionCompacted + | EventTodoUpdated | EventWorktreeReady | EventWorktreeFailed | EventPtyCreated | EventPtyUpdated | EventPtyExited | EventPtyDeleted - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceStatus | EventMessageUpdated | EventMessageRemoved | EventMessagePartUpdated diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 71b17966e..7bc6392b9 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7986,6 +7986,71 @@ }, "required": ["type", "properties"] }, + "Event.workspace.ready": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "workspace.ready" + }, + "properties": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"] + } + }, + "required": ["type", "properties"] + }, + "Event.workspace.failed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "workspace.failed" + }, + "properties": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + }, + "required": ["type", "properties"] + }, + "Event.workspace.status": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "workspace.status" + }, + "properties": { + "type": "object", + "properties": { + "workspaceID": { + "type": "string", + "pattern": "^wrk.*" + }, + "status": { + "type": "string", + "enum": ["connected", "connecting", "disconnected", "error"] + }, + "error": { + "type": "string" + } + }, + "required": ["workspaceID", "status"] + } + }, + "required": ["type", "properties"] + }, "QuestionOption": { "type": "object", "properties": { @@ -8136,50 +8201,6 @@ }, "required": ["type", "properties"] }, - "Todo": { - "type": "object", - "properties": { - "content": { - "description": "Brief description of the task", - "type": "string" - }, - "status": { - "description": "Current status of the task: pending, in_progress, completed, cancelled", - "type": "string" - }, - "priority": { - "description": "Priority level of the task: high, medium, low", - "type": "string" - } - }, - "required": ["content", "status", "priority"] - }, - "Event.todo.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "todo.updated" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses.*" - }, - "todos": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Todo" - } - } - }, - "required": ["sessionID", "todos"] - } - }, - "required": ["type", "properties"] - }, "SessionStatus": { "anyOf": [ { @@ -8286,6 +8307,50 @@ }, "required": ["type", "properties"] }, + "Todo": { + "type": "object", + "properties": { + "content": { + "description": "Brief description of the task", + "type": "string" + }, + "status": { + "description": "Current status of the task: pending, in_progress, completed, cancelled", + "type": "string" + }, + "priority": { + "description": "Priority level of the task: high, medium, low", + "type": "string" + } + }, + "required": ["content", "status", "priority"] + }, + "Event.todo.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "todo.updated" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses.*" + }, + "todos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": ["sessionID", "todos"] + } + }, + "required": ["type", "properties"] + }, "Event.worktree.ready": { "type": "object", "properties": { @@ -8440,71 +8505,6 @@ }, "required": ["type", "properties"] }, - "Event.workspace.ready": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "workspace.ready" - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": ["name"] - } - }, - "required": ["type", "properties"] - }, - "Event.workspace.failed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "workspace.failed" - }, - "properties": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"] - } - }, - "required": ["type", "properties"] - }, - "Event.workspace.status": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "workspace.status" - }, - "properties": { - "type": "object", - "properties": { - "workspaceID": { - "type": "string", - "pattern": "^wrk.*" - }, - "status": { - "type": "string", - "enum": ["connected", "connecting", "disconnected", "error"] - }, - "error": { - "type": "string" - } - }, - "required": ["workspaceID", "status"] - } - }, - "required": ["type", "properties"] - }, "OutputFormatText": { "type": "object", "properties": { @@ -9937,6 +9937,15 @@ { "$ref": "#/components/schemas/Event.command.executed" }, + { + "$ref": "#/components/schemas/Event.workspace.ready" + }, + { + "$ref": "#/components/schemas/Event.workspace.failed" + }, + { + "$ref": "#/components/schemas/Event.workspace.status" + }, { "$ref": "#/components/schemas/Event.question.asked" }, @@ -9946,9 +9955,6 @@ { "$ref": "#/components/schemas/Event.question.rejected" }, - { - "$ref": "#/components/schemas/Event.todo.updated" - }, { "$ref": "#/components/schemas/Event.session.status" }, @@ -9958,6 +9964,9 @@ { "$ref": "#/components/schemas/Event.session.compacted" }, + { + "$ref": "#/components/schemas/Event.todo.updated" + }, { "$ref": "#/components/schemas/Event.worktree.ready" }, @@ -9976,15 +9985,6 @@ { "$ref": "#/components/schemas/Event.pty.deleted" }, - { - "$ref": "#/components/schemas/Event.workspace.ready" - }, - { - "$ref": "#/components/schemas/Event.workspace.failed" - }, - { - "$ref": "#/components/schemas/Event.workspace.status" - }, { "$ref": "#/components/schemas/Event.message.updated" }, -- cgit v1.2.3 From cb1e5d9e41b451dbf44ee565d61c6bd426f9175a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 11 Apr 2026 20:56:22 +0000 Subject: chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 52 +++++------ packages/sdk/openapi.json | 148 ++++++++++++++++---------------- 2 files changed, 100 insertions(+), 100 deletions(-) (limited to 'packages/sdk/openapi.json') diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 7ca6ea05c..de45b1599 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -316,29 +316,6 @@ export type EventCommandExecuted = { } } -export type EventWorkspaceReady = { - type: "workspace.ready" - properties: { - name: string - } -} - -export type EventWorkspaceFailed = { - type: "workspace.failed" - properties: { - message: string - } -} - -export type EventWorkspaceStatus = { - type: "workspace.status" - properties: { - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" - error?: string - } -} - export type QuestionOption = { /** * Display text (1-5 words, concise) @@ -523,6 +500,29 @@ export type EventPtyDeleted = { } } +export type EventWorkspaceReady = { + type: "workspace.ready" + properties: { + name: string + } +} + +export type EventWorkspaceFailed = { + type: "workspace.failed" + properties: { + message: string + } +} + +export type EventWorkspaceStatus = { + type: "workspace.status" + properties: { + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" + error?: string + } +} + export type OutputFormatText = { type: "text" } @@ -995,9 +995,6 @@ export type Event = | EventMcpToolsChanged | EventMcpBrowserOpenFailed | EventCommandExecuted - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceStatus | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected @@ -1011,6 +1008,9 @@ export type Event = | EventPtyUpdated | EventPtyExited | EventPtyDeleted + | EventWorkspaceReady + | EventWorkspaceFailed + | EventWorkspaceStatus | EventMessageUpdated | EventMessageRemoved | EventMessagePartUpdated diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 7bc6392b9..8bcae7d03 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7986,71 +7986,6 @@ }, "required": ["type", "properties"] }, - "Event.workspace.ready": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "workspace.ready" - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": ["name"] - } - }, - "required": ["type", "properties"] - }, - "Event.workspace.failed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "workspace.failed" - }, - "properties": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": ["message"] - } - }, - "required": ["type", "properties"] - }, - "Event.workspace.status": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "workspace.status" - }, - "properties": { - "type": "object", - "properties": { - "workspaceID": { - "type": "string", - "pattern": "^wrk.*" - }, - "status": { - "type": "string", - "enum": ["connected", "connecting", "disconnected", "error"] - }, - "error": { - "type": "string" - } - }, - "required": ["workspaceID", "status"] - } - }, - "required": ["type", "properties"] - }, "QuestionOption": { "type": "object", "properties": { @@ -8505,6 +8440,71 @@ }, "required": ["type", "properties"] }, + "Event.workspace.ready": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "workspace.ready" + }, + "properties": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"] + } + }, + "required": ["type", "properties"] + }, + "Event.workspace.failed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "workspace.failed" + }, + "properties": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + }, + "required": ["type", "properties"] + }, + "Event.workspace.status": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "workspace.status" + }, + "properties": { + "type": "object", + "properties": { + "workspaceID": { + "type": "string", + "pattern": "^wrk.*" + }, + "status": { + "type": "string", + "enum": ["connected", "connecting", "disconnected", "error"] + }, + "error": { + "type": "string" + } + }, + "required": ["workspaceID", "status"] + } + }, + "required": ["type", "properties"] + }, "OutputFormatText": { "type": "object", "properties": { @@ -9937,15 +9937,6 @@ { "$ref": "#/components/schemas/Event.command.executed" }, - { - "$ref": "#/components/schemas/Event.workspace.ready" - }, - { - "$ref": "#/components/schemas/Event.workspace.failed" - }, - { - "$ref": "#/components/schemas/Event.workspace.status" - }, { "$ref": "#/components/schemas/Event.question.asked" }, @@ -9985,6 +9976,15 @@ { "$ref": "#/components/schemas/Event.pty.deleted" }, + { + "$ref": "#/components/schemas/Event.workspace.ready" + }, + { + "$ref": "#/components/schemas/Event.workspace.failed" + }, + { + "$ref": "#/components/schemas/Event.workspace.status" + }, { "$ref": "#/components/schemas/Event.message.updated" }, -- cgit v1.2.3 From 4c4eef46f1c404a4ad2b205ae954bf4a17ef33a5 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 11 Apr 2026 22:15:53 +0000 Subject: chore: generate --- packages/opencode/src/session/index.ts | 5 ++--- packages/sdk/openapi.json | 3 +++ 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'packages/sdk/openapi.json') diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 70deb95a4..3d4903588 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -708,9 +708,8 @@ export namespace Session { runPromise((svc) => svc.setArchived(input)), ) - export const setPermission = fn( - z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset }), - (input) => runPromise((svc) => svc.setPermission(input)), + export const setPermission = fn(z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset }), (input) => + runPromise((svc) => svc.setPermission(input)), ) export const setRevert = fn( diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 8bcae7d03..5e5058fde 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -2550,6 +2550,9 @@ "title": { "type": "string" }, + "permission": { + "$ref": "#/components/schemas/PermissionRuleset" + }, "time": { "type": "object", "properties": { -- cgit v1.2.3 From 17b290088485bd8c1ccdfbf13e8e9c2f3b2c3301 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 12 Apr 2026 00:58:05 +0000 Subject: chore: generate --- packages/opencode/test/question/question.test.ts | 7 +- packages/sdk/js/src/v2/gen/types.gen.ts | 48 ++++++------ packages/sdk/openapi.json | 94 ++++++++++++------------ 3 files changed, 73 insertions(+), 76 deletions(-) (limited to 'packages/sdk/openapi.json') diff --git a/packages/opencode/test/question/question.test.ts b/packages/opencode/test/question/question.test.ts index 8ee361e57..7c101ce28 100644 --- a/packages/opencode/test/question/question.test.ts +++ b/packages/opencode/test/question/question.test.ts @@ -6,11 +6,8 @@ import { tmpdir } from "../fixture/fixture" import { SessionID } from "../../src/session/schema" import { AppRuntime } from "../../src/effect/app-runtime" -const ask = (input: { - sessionID: SessionID - questions: Question.Info[] - tool?: { messageID: any; callID: string } -}) => AppRuntime.runPromise(Question.Service.use((svc) => svc.ask(input))) +const ask = (input: { sessionID: SessionID; questions: Question.Info[]; tool?: { messageID: any; callID: string } }) => + AppRuntime.runPromise(Question.Service.use((svc) => svc.ask(input))) const list = () => AppRuntime.runPromise(Question.Service.use((svc) => svc.list())) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index f2b32a159..4aab9f439 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -387,6 +387,29 @@ export type EventQuestionRejected = { } } +export type Todo = { + /** + * Brief description of the task + */ + content: string + /** + * Current status of the task: pending, in_progress, completed, cancelled + */ + status: string + /** + * Priority level of the task: high, medium, low + */ + priority: string +} + +export type EventTodoUpdated = { + type: "todo.updated" + properties: { + sessionID: string + todos: Array + } +} + export type SessionStatus = | { type: "idle" @@ -423,29 +446,6 @@ export type EventSessionCompacted = { } } -export type Todo = { - /** - * Brief description of the task - */ - content: string - /** - * Current status of the task: pending, in_progress, completed, cancelled - */ - status: string - /** - * Priority level of the task: high, medium, low - */ - priority: string -} - -export type EventTodoUpdated = { - type: "todo.updated" - properties: { - sessionID: string - todos: Array - } -} - export type EventWorktreeReady = { type: "worktree.ready" properties: { @@ -998,10 +998,10 @@ export type Event = | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected + | EventTodoUpdated | EventSessionStatus | EventSessionIdle | EventSessionCompacted - | EventTodoUpdated | EventWorktreeReady | EventWorktreeFailed | EventPtyCreated diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 5e5058fde..e8d6e6a08 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8139,6 +8139,50 @@ }, "required": ["type", "properties"] }, + "Todo": { + "type": "object", + "properties": { + "content": { + "description": "Brief description of the task", + "type": "string" + }, + "status": { + "description": "Current status of the task: pending, in_progress, completed, cancelled", + "type": "string" + }, + "priority": { + "description": "Priority level of the task: high, medium, low", + "type": "string" + } + }, + "required": ["content", "status", "priority"] + }, + "Event.todo.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "todo.updated" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses.*" + }, + "todos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": ["sessionID", "todos"] + } + }, + "required": ["type", "properties"] + }, "SessionStatus": { "anyOf": [ { @@ -8245,50 +8289,6 @@ }, "required": ["type", "properties"] }, - "Todo": { - "type": "object", - "properties": { - "content": { - "description": "Brief description of the task", - "type": "string" - }, - "status": { - "description": "Current status of the task: pending, in_progress, completed, cancelled", - "type": "string" - }, - "priority": { - "description": "Priority level of the task: high, medium, low", - "type": "string" - } - }, - "required": ["content", "status", "priority"] - }, - "Event.todo.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "todo.updated" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses.*" - }, - "todos": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Todo" - } - } - }, - "required": ["sessionID", "todos"] - } - }, - "required": ["type", "properties"] - }, "Event.worktree.ready": { "type": "object", "properties": { @@ -9949,6 +9949,9 @@ { "$ref": "#/components/schemas/Event.question.rejected" }, + { + "$ref": "#/components/schemas/Event.todo.updated" + }, { "$ref": "#/components/schemas/Event.session.status" }, @@ -9958,9 +9961,6 @@ { "$ref": "#/components/schemas/Event.session.compacted" }, - { - "$ref": "#/components/schemas/Event.todo.updated" - }, { "$ref": "#/components/schemas/Event.worktree.ready" }, -- cgit v1.2.3 From 7164662be2fcf72410e3f895c9dee9b564b750a3 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 13 Apr 2026 14:18:05 +0000 Subject: chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 238 ++++++------- packages/sdk/openapi.json | 588 ++++++++++++++++---------------- 2 files changed, 413 insertions(+), 413 deletions(-) (limited to 'packages/sdk/openapi.json') diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 4aab9f439..6512f40a2 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -33,6 +33,13 @@ export type EventProjectUpdated = { properties: Project } +export type EventServerInstanceDisposed = { + type: "server.instance.disposed" + properties: { + directory: string + } +} + export type EventInstallationUpdated = { type: "installation.updated" properties: { @@ -47,13 +54,6 @@ export type EventInstallationUpdateAvailable = { } } -export type EventServerInstanceDisposed = { - type: "server.instance.disposed" - properties: { - directory: string - } -} - export type EventServerConnected = { type: "server.connected" properties: { @@ -68,6 +68,21 @@ export type EventGlobalDisposed = { } } +export type EventFileEdited = { + type: "file.edited" + properties: { + file: string + } +} + +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } +} + export type EventLspClientDiagnostics = { type: "lsp.client.diagnostics" properties: { @@ -215,107 +230,6 @@ export type EventSessionError = { } } -export type EventFileEdited = { - type: "file.edited" - properties: { - file: string - } -} - -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - -export type EventVcsBranchUpdated = { - type: "vcs.branch.updated" - properties: { - branch?: string - } -} - -export type EventTuiPromptAppend = { - type: "tui.prompt.append" - properties: { - text: string - } -} - -export type EventTuiCommandExecute = { - type: "tui.command.execute" - properties: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.line.up" - | "session.line.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } -} - -export type EventTuiToastShow = { - type: "tui.toast.show" - properties: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - /** - * Duration in milliseconds - */ - duration?: number - } -} - -export type EventTuiSessionSelect = { - type: "tui.session.select" - properties: { - /** - * Session ID to navigate to - */ - sessionID: string - } -} - -export type EventMcpToolsChanged = { - type: "mcp.tools.changed" - properties: { - server: string - } -} - -export type EventMcpBrowserOpenFailed = { - type: "mcp.browser.open.failed" - properties: { - mcpName: string - url: string - } -} - -export type EventCommandExecuted = { - type: "command.executed" - properties: { - name: string - sessionID: string - arguments: string - messageID: string - } -} - export type QuestionOption = { /** * Display text (1-5 words, concise) @@ -446,6 +360,92 @@ export type EventSessionCompacted = { } } +export type EventTuiPromptAppend = { + type: "tui.prompt.append" + properties: { + text: string + } +} + +export type EventTuiCommandExecute = { + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.line.up" + | "session.line.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } +} + +export type EventTuiToastShow = { + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + /** + * Duration in milliseconds + */ + duration?: number + } +} + +export type EventTuiSessionSelect = { + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string + } +} + +export type EventMcpToolsChanged = { + type: "mcp.tools.changed" + properties: { + server: string + } +} + +export type EventMcpBrowserOpenFailed = { + type: "mcp.browser.open.failed" + properties: { + mcpName: string + url: string + } +} + +export type EventCommandExecuted = { + type: "command.executed" + properties: { + name: string + sessionID: string + arguments: string + messageID: string + } +} + +export type EventVcsBranchUpdated = { + type: "vcs.branch.updated" + properties: { + branch?: string + } +} + export type EventWorktreeReady = { type: "worktree.ready" properties: { @@ -973,11 +973,13 @@ export type EventSessionDeleted = { export type Event = | EventProjectUpdated + | EventServerInstanceDisposed | EventInstallationUpdated | EventInstallationUpdateAvailable - | EventServerInstanceDisposed | EventServerConnected | EventGlobalDisposed + | EventFileEdited + | EventFileWatcherUpdated | EventLspClientDiagnostics | EventLspUpdated | EventMessagePartDelta @@ -985,16 +987,6 @@ export type Event = | EventPermissionReplied | EventSessionDiff | EventSessionError - | EventFileEdited - | EventFileWatcherUpdated - | EventVcsBranchUpdated - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventMcpBrowserOpenFailed - | EventCommandExecuted | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected @@ -1002,6 +994,14 @@ export type Event = | EventSessionStatus | EventSessionIdle | EventSessionCompacted + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventMcpBrowserOpenFailed + | EventCommandExecuted + | EventVcsBranchUpdated | EventWorktreeReady | EventWorktreeFailed | EventPtyCreated diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index e8d6e6a08..5369d1c2e 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7230,31 +7230,31 @@ }, "required": ["type", "properties"] }, - "Event.installation.updated": { + "Event.server.instance.disposed": { "type": "object", "properties": { "type": { "type": "string", - "const": "installation.updated" + "const": "server.instance.disposed" }, "properties": { "type": "object", "properties": { - "version": { + "directory": { "type": "string" } }, - "required": ["version"] + "required": ["directory"] } }, "required": ["type", "properties"] }, - "Event.installation.update-available": { + "Event.installation.updated": { "type": "object", "properties": { "type": { "type": "string", - "const": "installation.update-available" + "const": "installation.updated" }, "properties": { "type": "object", @@ -7268,21 +7268,21 @@ }, "required": ["type", "properties"] }, - "Event.server.instance.disposed": { + "Event.installation.update-available": { "type": "object", "properties": { "type": { "type": "string", - "const": "server.instance.disposed" + "const": "installation.update-available" }, "properties": { "type": "object", "properties": { - "directory": { + "version": { "type": "string" } }, - "required": ["directory"] + "required": ["version"] } }, "required": ["type", "properties"] @@ -7315,6 +7315,60 @@ }, "required": ["type", "properties"] }, + "Event.file.edited": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "file.edited" + }, + "properties": { + "type": "object", + "properties": { + "file": { + "type": "string" + } + }, + "required": ["file"] + } + }, + "required": ["type", "properties"] + }, + "Event.file.watcher.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "file.watcher.updated" + }, + "properties": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "event": { + "anyOf": [ + { + "type": "string", + "const": "add" + }, + { + "type": "string", + "const": "change" + }, + { + "type": "string", + "const": "unlink" + } + ] + } + }, + "required": ["file", "event"] + } + }, + "required": ["type", "properties"] + }, "Event.lsp.client.diagnostics": { "type": "object", "properties": { @@ -7724,267 +7778,9 @@ { "$ref": "#/components/schemas/APIError" } - ] - } - } - } - }, - "required": ["type", "properties"] - }, - "Event.file.edited": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "file.edited" - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - } - }, - "required": ["file"] - } - }, - "required": ["type", "properties"] - }, - "Event.file.watcher.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "file.watcher.updated" - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "event": { - "anyOf": [ - { - "type": "string", - "const": "add" - }, - { - "type": "string", - "const": "change" - }, - { - "type": "string", - "const": "unlink" - } - ] - } - }, - "required": ["file", "event"] - } - }, - "required": ["type", "properties"] - }, - "Event.vcs.branch.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "vcs.branch.updated" - }, - "properties": { - "type": "object", - "properties": { - "branch": { - "type": "string" - } - } - } - }, - "required": ["type", "properties"] - }, - "Event.tui.prompt.append": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "tui.prompt.append" - }, - "properties": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"] - } - }, - "required": ["type", "properties"] - }, - "Event.tui.command.execute": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "tui.command.execute" - }, - "properties": { - "type": "object", - "properties": { - "command": { - "anyOf": [ - { - "type": "string", - "enum": [ - "session.list", - "session.new", - "session.share", - "session.interrupt", - "session.compact", - "session.page.up", - "session.page.down", - "session.line.up", - "session.line.down", - "session.half.page.up", - "session.half.page.down", - "session.first", - "session.last", - "prompt.clear", - "prompt.submit", - "agent.cycle" - ] - }, - { - "type": "string" - } - ] - } - }, - "required": ["command"] - } - }, - "required": ["type", "properties"] - }, - "Event.tui.toast.show": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "tui.toast.show" - }, - "properties": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "description": "Duration in milliseconds", - "default": 5000, - "type": "number" - } - }, - "required": ["message", "variant"] - } - }, - "required": ["type", "properties"] - }, - "Event.tui.session.select": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "tui.session.select" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "description": "Session ID to navigate to", - "type": "string", - "pattern": "^ses.*" - } - }, - "required": ["sessionID"] - } - }, - "required": ["type", "properties"] - }, - "Event.mcp.tools.changed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "mcp.tools.changed" - }, - "properties": { - "type": "object", - "properties": { - "server": { - "type": "string" - } - }, - "required": ["server"] - } - }, - "required": ["type", "properties"] - }, - "Event.mcp.browser.open.failed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "mcp.browser.open.failed" - }, - "properties": { - "type": "object", - "properties": { - "mcpName": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": ["mcpName", "url"] - } - }, - "required": ["type", "properties"] - }, - "Event.command.executed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "command.executed" - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "sessionID": { - "type": "string", - "pattern": "^ses.*" - }, - "arguments": { - "type": "string" - }, - "messageID": { - "type": "string", - "pattern": "^msg.*" + ] } - }, - "required": ["name", "sessionID", "arguments", "messageID"] + } } }, "required": ["type", "properties"] @@ -8289,6 +8085,210 @@ }, "required": ["type", "properties"] }, + "Event.tui.prompt.append": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tui.prompt.append" + }, + "properties": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"] + } + }, + "required": ["type", "properties"] + }, + "Event.tui.command.execute": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tui.command.execute" + }, + "properties": { + "type": "object", + "properties": { + "command": { + "anyOf": [ + { + "type": "string", + "enum": [ + "session.list", + "session.new", + "session.share", + "session.interrupt", + "session.compact", + "session.page.up", + "session.page.down", + "session.line.up", + "session.line.down", + "session.half.page.up", + "session.half.page.down", + "session.first", + "session.last", + "prompt.clear", + "prompt.submit", + "agent.cycle" + ] + }, + { + "type": "string" + } + ] + } + }, + "required": ["command"] + } + }, + "required": ["type", "properties"] + }, + "Event.tui.toast.show": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tui.toast.show" + }, + "properties": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "variant": { + "type": "string", + "enum": ["info", "success", "warning", "error"] + }, + "duration": { + "description": "Duration in milliseconds", + "default": 5000, + "type": "number" + } + }, + "required": ["message", "variant"] + } + }, + "required": ["type", "properties"] + }, + "Event.tui.session.select": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tui.session.select" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "description": "Session ID to navigate to", + "type": "string", + "pattern": "^ses.*" + } + }, + "required": ["sessionID"] + } + }, + "required": ["type", "properties"] + }, + "Event.mcp.tools.changed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "mcp.tools.changed" + }, + "properties": { + "type": "object", + "properties": { + "server": { + "type": "string" + } + }, + "required": ["server"] + } + }, + "required": ["type", "properties"] + }, + "Event.mcp.browser.open.failed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "mcp.browser.open.failed" + }, + "properties": { + "type": "object", + "properties": { + "mcpName": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": ["mcpName", "url"] + } + }, + "required": ["type", "properties"] + }, + "Event.command.executed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "command.executed" + }, + "properties": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "sessionID": { + "type": "string", + "pattern": "^ses.*" + }, + "arguments": { + "type": "string" + }, + "messageID": { + "type": "string", + "pattern": "^msg.*" + } + }, + "required": ["name", "sessionID", "arguments", "messageID"] + } + }, + "required": ["type", "properties"] + }, + "Event.vcs.branch.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "vcs.branch.updated" + }, + "properties": { + "type": "object", + "properties": { + "branch": { + "type": "string" + } + } + } + }, + "required": ["type", "properties"] + }, "Event.worktree.ready": { "type": "object", "properties": { @@ -9875,13 +9875,13 @@ "$ref": "#/components/schemas/Event.project.updated" }, { - "$ref": "#/components/schemas/Event.installation.updated" + "$ref": "#/components/schemas/Event.server.instance.disposed" }, { - "$ref": "#/components/schemas/Event.installation.update-available" + "$ref": "#/components/schemas/Event.installation.updated" }, { - "$ref": "#/components/schemas/Event.server.instance.disposed" + "$ref": "#/components/schemas/Event.installation.update-available" }, { "$ref": "#/components/schemas/Event.server.connected" @@ -9889,6 +9889,12 @@ { "$ref": "#/components/schemas/Event.global.disposed" }, + { + "$ref": "#/components/schemas/Event.file.edited" + }, + { + "$ref": "#/components/schemas/Event.file.watcher.updated" + }, { "$ref": "#/components/schemas/Event.lsp.client.diagnostics" }, @@ -9911,55 +9917,49 @@ "$ref": "#/components/schemas/Event.session.error" }, { - "$ref": "#/components/schemas/Event.file.edited" - }, - { - "$ref": "#/components/schemas/Event.file.watcher.updated" - }, - { - "$ref": "#/components/schemas/Event.vcs.branch.updated" + "$ref": "#/components/schemas/Event.question.asked" }, { - "$ref": "#/components/schemas/Event.tui.prompt.append" + "$ref": "#/components/schemas/Event.question.replied" }, { - "$ref": "#/components/schemas/Event.tui.command.execute" + "$ref": "#/components/schemas/Event.question.rejected" }, { - "$ref": "#/components/schemas/Event.tui.toast.show" + "$ref": "#/components/schemas/Event.todo.updated" }, { - "$ref": "#/components/schemas/Event.tui.session.select" + "$ref": "#/components/schemas/Event.session.status" }, { - "$ref": "#/components/schemas/Event.mcp.tools.changed" + "$ref": "#/components/schemas/Event.session.idle" }, { - "$ref": "#/components/schemas/Event.mcp.browser.open.failed" + "$ref": "#/components/schemas/Event.session.compacted" }, { - "$ref": "#/components/schemas/Event.command.executed" + "$ref": "#/components/schemas/Event.tui.prompt.append" }, { - "$ref": "#/components/schemas/Event.question.asked" + "$ref": "#/components/schemas/Event.tui.command.execute" }, { - "$ref": "#/components/schemas/Event.question.replied" + "$ref": "#/components/schemas/Event.tui.toast.show" }, { - "$ref": "#/components/schemas/Event.question.rejected" + "$ref": "#/components/schemas/Event.tui.session.select" }, { - "$ref": "#/components/schemas/Event.todo.updated" + "$ref": "#/components/schemas/Event.mcp.tools.changed" }, { - "$ref": "#/components/schemas/Event.session.status" + "$ref": "#/components/schemas/Event.mcp.browser.open.failed" }, { - "$ref": "#/components/schemas/Event.session.idle" + "$ref": "#/components/schemas/Event.command.executed" }, { - "$ref": "#/components/schemas/Event.session.compacted" + "$ref": "#/components/schemas/Event.vcs.branch.updated" }, { "$ref": "#/components/schemas/Event.worktree.ready" -- cgit v1.2.3 From 329fcb040be17a5779c3b88cab9367127e491638 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 13 Apr 2026 17:37:41 +0000 Subject: chore: generate --- .../20260410174513_workspace-name/snapshot.json | 132 ++++++--------------- packages/sdk/js/src/v2/gen/sdk.gen.ts | 38 ++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 26 +++- packages/sdk/openapi.json | 71 +++++++++-- 4 files changed, 156 insertions(+), 111 deletions(-) (limited to 'packages/sdk/openapi.json') diff --git a/packages/opencode/migration/20260410174513_workspace-name/snapshot.json b/packages/opencode/migration/20260410174513_workspace-name/snapshot.json index ab7028008..9adeeecbe 100644 --- a/packages/opencode/migration/20260410174513_workspace-name/snapshot.json +++ b/packages/opencode/migration/20260410174513_workspace-name/snapshot.json @@ -2,9 +2,7 @@ "version": "7", "dialect": "sqlite", "id": "b61476b8-3b92-49ae-9fa5-6eef586ed64b", - "prevIds": [ - "f13dfa58-7fb4-47a2-8f6b-dc70258e14ed" - ], + "prevIds": ["f13dfa58-7fb4-47a2-8f6b-dc70258e14ed"], "ddl": [ { "name": "account_state", @@ -969,13 +967,9 @@ "table": "event" }, { - "columns": [ - "active_account_id" - ], + "columns": ["active_account_id"], "tableTo": "account", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "NO ACTION", "onDelete": "SET NULL", "nameExplicit": false, @@ -984,13 +978,9 @@ "table": "account_state" }, { - "columns": [ - "project_id" - ], + "columns": ["project_id"], "tableTo": "project", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -999,13 +989,9 @@ "table": "workspace" }, { - "columns": [ - "session_id" - ], + "columns": ["session_id"], "tableTo": "session", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1014,13 +1000,9 @@ "table": "message" }, { - "columns": [ - "message_id" - ], + "columns": ["message_id"], "tableTo": "message", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1029,13 +1011,9 @@ "table": "part" }, { - "columns": [ - "project_id" - ], + "columns": ["project_id"], "tableTo": "project", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1044,13 +1022,9 @@ "table": "permission" }, { - "columns": [ - "project_id" - ], + "columns": ["project_id"], "tableTo": "project", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1059,13 +1033,9 @@ "table": "session" }, { - "columns": [ - "session_id" - ], + "columns": ["session_id"], "tableTo": "session", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1074,13 +1044,9 @@ "table": "todo" }, { - "columns": [ - "session_id" - ], + "columns": ["session_id"], "tableTo": "session", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1089,13 +1055,9 @@ "table": "session_share" }, { - "columns": [ - "aggregate_id" - ], + "columns": ["aggregate_id"], "tableTo": "event_sequence", - "columnsTo": [ - "aggregate_id" - ], + "columnsTo": ["aggregate_id"], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1104,119 +1066,91 @@ "table": "event" }, { - "columns": [ - "email", - "url" - ], + "columns": ["email", "url"], "nameExplicit": false, "name": "control_account_pk", "entityType": "pks", "table": "control_account" }, { - "columns": [ - "session_id", - "position" - ], + "columns": ["session_id", "position"], "nameExplicit": false, "name": "todo_pk", "entityType": "pks", "table": "todo" }, { - "columns": [ - "id" - ], + "columns": ["id"], "nameExplicit": false, "name": "account_state_pk", "table": "account_state", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "nameExplicit": false, "name": "account_pk", "table": "account", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "nameExplicit": false, "name": "workspace_pk", "table": "workspace", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "nameExplicit": false, "name": "project_pk", "table": "project", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "nameExplicit": false, "name": "message_pk", "table": "message", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "nameExplicit": false, "name": "part_pk", "table": "part", "entityType": "pks" }, { - "columns": [ - "project_id" - ], + "columns": ["project_id"], "nameExplicit": false, "name": "permission_pk", "table": "permission", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "nameExplicit": false, "name": "session_pk", "table": "session", "entityType": "pks" }, { - "columns": [ - "session_id" - ], + "columns": ["session_id"], "nameExplicit": false, "name": "session_share_pk", "table": "session_share", "entityType": "pks" }, { - "columns": [ - "aggregate_id" - ], + "columns": ["aggregate_id"], "nameExplicit": false, "name": "event_sequence_pk", "table": "event_sequence", "entityType": "pks" }, { - "columns": [ - "id" - ], + "columns": ["id"], "nameExplicit": false, "name": "event_pk", "table": "event", @@ -1334,4 +1268,4 @@ } ], "renames": [] -} \ No newline at end of file +} diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 7fba60fce..f297cb18e 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -29,6 +29,7 @@ import type { ExperimentalConsoleSwitchOrgResponses, ExperimentalResourceListResponses, ExperimentalSessionListResponses, + ExperimentalWorkspaceAdaptorListResponses, ExperimentalWorkspaceCreateErrors, ExperimentalWorkspaceCreateResponses, ExperimentalWorkspaceListResponses, @@ -1086,6 +1087,38 @@ export class Console extends HeyApiClient { } } +export class Adaptor extends HeyApiClient { + /** + * List workspace adaptors + * + * List all available workspace adaptors for the current project. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/workspace/adaptor", + ...options, + ...params, + }) + } +} + export class Workspace extends HeyApiClient { /** * List workspaces @@ -1229,6 +1262,11 @@ export class Workspace extends HeyApiClient { ...params, }) } + + private _adaptor?: Adaptor + get adaptor(): Adaptor { + return (this._adaptor ??= new Adaptor({ client: this.client })) + } } export class Session extends HeyApiClient { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 6512f40a2..83ec3d751 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1772,8 +1772,8 @@ export type ToolList = Array export type Workspace = { id: string type: string + name: string branch: string | null - name: string | null directory: string | null extra: unknown | null projectID: string @@ -2812,6 +2812,30 @@ export type ToolListResponses = { export type ToolListResponse = ToolListResponses[keyof ToolListResponses] +export type ExperimentalWorkspaceAdaptorListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/adaptor" +} + +export type ExperimentalWorkspaceAdaptorListResponses = { + /** + * Workspace adaptors + */ + 200: Array<{ + type: string + name: string + description: string + }> +} + +export type ExperimentalWorkspaceAdaptorListResponse = + ExperimentalWorkspaceAdaptorListResponses[keyof ExperimentalWorkspaceAdaptorListResponses] + export type ExperimentalWorkspaceListData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 5369d1c2e..85443726f 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -1526,6 +1526,62 @@ ] } }, + "/experimental/workspace/adaptor": { + "get": { + "operationId": "experimental.workspace.adaptor.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "List workspace adaptors", + "description": "List all available workspace adaptors for the current project.", + "responses": { + "200": { + "description": "Workspace adaptors", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["type", "name", "description"] + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.adaptor.list({\n ...\n})" + } + ] + } + }, "/experimental/workspace": { "post": { "operationId": "experimental.workspace.create", @@ -11885,17 +11941,10 @@ "type": { "type": "string" }, - "branch": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, "name": { + "type": "string" + }, + "branch": { "anyOf": [ { "type": "string" @@ -11927,7 +11976,7 @@ "type": "string" } }, - "required": ["id", "type", "branch", "name", "directory", "extra", "projectID"] + "required": ["id", "type", "name", "branch", "directory", "extra", "projectID"] }, "Worktree": { "type": "object", -- cgit v1.2.3 From b22add292c194697c1581baf1a323996f966d04b Mon Sep 17 00:00:00 2001 From: James Long Date: Mon, 13 Apr 2026 16:51:59 -0400 Subject: refactor(core): publish sync events to global event stream (#22347) --- packages/app/src/context/global-sdk.tsx | 7 +- packages/opencode/src/bus/bus-event.ts | 29 +- packages/opencode/src/cli/cmd/tui/context/event.ts | 4 + packages/opencode/src/project/instance.ts | 2 - packages/opencode/src/server/instance/event.ts | 8 +- packages/opencode/src/server/instance/global.ts | 48 +- packages/opencode/src/sync/index.ts | 59 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 20 - packages/sdk/js/src/v2/gen/types.gen.ts | 229 +++++--- packages/sdk/openapi.json | 600 ++++++++++++++------- 10 files changed, 597 insertions(+), 409 deletions(-) (limited to 'packages/sdk/openapi.json') diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx index 1205a8fa8..172b5c966 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -155,7 +155,12 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo resetHeartbeat() streamErrorLogged = false const directory = event.directory ?? "global" - const payload = event.payload + if (event.payload.type === "sync") { + continue + } + + const payload = event.payload as Event + const k = key(directory, payload) if (k) { const i = coalesced.get(k) diff --git a/packages/opencode/src/bus/bus-event.ts b/packages/opencode/src/bus/bus-event.ts index d97922290..aad5f398e 100644 --- a/packages/opencode/src/bus/bus-event.ts +++ b/packages/opencode/src/bus/bus-event.ts @@ -16,25 +16,18 @@ export namespace BusEvent { } export function payloads() { - return z - .discriminatedUnion( - "type", - registry - .entries() - .map(([type, def]) => { - return z - .object({ - type: z.literal(type), - properties: def.properties, - }) - .meta({ - ref: "Event" + "." + def.type, - }) + return registry + .entries() + .map(([type, def]) => { + return z + .object({ + type: z.literal(type), + properties: def.properties, + }) + .meta({ + ref: "Event" + "." + def.type, }) - .toArray() as any, - ) - .meta({ - ref: "Event", }) + .toArray() } } diff --git a/packages/opencode/src/cli/cmd/tui/context/event.ts b/packages/opencode/src/cli/cmd/tui/context/event.ts index da073f6e9..156f9c947 100644 --- a/packages/opencode/src/cli/cmd/tui/context/event.ts +++ b/packages/opencode/src/cli/cmd/tui/context/event.ts @@ -8,6 +8,10 @@ export function useEvent() { function subscribe(handler: (event: Event) => void) { return sdk.event.on("event", (event) => { + if (event.payload.type === "sync") { + return + } + // Special hack for truly global events if (event.directory === "global") { handler(event.payload) diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index 8d2d51db6..12de88a27 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -21,8 +21,6 @@ const disposal = { all: undefined as Promise | undefined, } -function emitDisposed(directory: string) {} - function boot(input: { directory: string; init?: () => Promise; worktree?: string; project?: Project.Info }) { return iife(async () => { const ctx = diff --git a/packages/opencode/src/server/instance/event.ts b/packages/opencode/src/server/instance/event.ts index 989b85771..5d631d954 100644 --- a/packages/opencode/src/server/instance/event.ts +++ b/packages/opencode/src/server/instance/event.ts @@ -1,8 +1,10 @@ +import z from "zod" import { Hono } from "hono" import { describeRoute, resolver } from "hono-openapi" import { streamSSE } from "hono/streaming" import { Log } from "@/util/log" import { BusEvent } from "@/bus/bus-event" +import { SyncEvent } from "@/sync" import { Bus } from "@/bus" import { AsyncQueue } from "../../util/queue" @@ -20,7 +22,11 @@ export const EventRoutes = () => description: "Event stream", content: { "text/event-stream": { - schema: resolver(BusEvent.payloads()), + schema: resolver( + z.union(BusEvent.payloads()).meta({ + ref: "Event", + }), + ), }, }, }, diff --git a/packages/opencode/src/server/instance/global.ts b/packages/opencode/src/server/instance/global.ts index daecb6bd3..d462a07f7 100644 --- a/packages/opencode/src/server/instance/global.ts +++ b/packages/opencode/src/server/instance/global.ts @@ -109,7 +109,7 @@ export const GlobalRoutes = lazy(() => directory: z.string(), project: z.string().optional(), workspace: z.string().optional(), - payload: BusEvent.payloads(), + payload: z.union([...BusEvent.payloads(), ...SyncEvent.payloads()]), }) .meta({ ref: "GlobalEvent", @@ -135,52 +135,6 @@ export const GlobalRoutes = lazy(() => }) }, ) - .get( - "/sync-event", - describeRoute({ - summary: "Subscribe to global sync events", - description: "Get global sync events", - operationId: "global.sync-event.subscribe", - responses: { - 200: { - description: "Event stream", - content: { - "text/event-stream": { - schema: resolver( - z - .object({ - payload: SyncEvent.payloads(), - }) - .meta({ - ref: "SyncEvent", - }), - ), - }, - }, - }, - }, - }), - async (c) => { - log.info("global sync event connected") - c.header("Cache-Control", "no-cache, no-transform") - c.header("X-Accel-Buffering", "no") - c.header("X-Content-Type-Options", "nosniff") - return streamEvents(c, (q) => { - return SyncEvent.subscribeAll(({ def, event }) => { - // TODO: don't pass def, just pass the type (and it should - // be versioned) - q.push( - JSON.stringify({ - payload: { - ...event, - type: SyncEvent.versionedType(def.type, def.version), - }, - }), - ) - }) - }) - }, - ) .get( "/config", describeRoute({ diff --git a/packages/opencode/src/sync/index.ts b/packages/opencode/src/sync/index.ts index a40939191..d7cb7f774 100644 --- a/packages/opencode/src/sync/index.ts +++ b/packages/opencode/src/sync/index.ts @@ -2,9 +2,12 @@ import z from "zod" import type { ZodObject } from "zod" import { EventEmitter } from "events" import { Database, eq } from "@/storage/db" +import { GlobalBus } from "@/bus/global" import { Bus as ProjectBus } from "@/bus" import { BusEvent } from "@/bus/bus-event" +import { Instance } from "@/project/instance" import { EventSequenceTable, EventTable } from "./event.sql" +import { WorkspaceContext } from "@/control-plane/workspace-context" import { EventID } from "./schema" import { Flag } from "@/flag/flag" @@ -37,8 +40,6 @@ export namespace SyncEvent { let frozen = false let convertEvent: (type: string, event: Event["data"]) => Promise> | Record - const Bus = new EventEmitter<{ event: [{ def: Definition; event: Event }] }>() - export function reset() { frozen = false projectors = undefined @@ -140,11 +141,6 @@ export namespace SyncEvent { } Database.effect(() => { - Bus.emit("event", { - def, - event, - }) - if (options?.publish) { const result = convertEvent(def.type, event.data) if (result instanceof Promise) { @@ -154,6 +150,17 @@ export namespace SyncEvent { } else { ProjectBus.publish({ type: def.type, properties: def.schema }, result) } + + GlobalBus.emit("event", { + directory: Instance.directory, + project: Instance.project.id, + workspace: WorkspaceContext.workspaceID, + payload: { + type: "sync", + name: versionedType(def.type, def.version), + ...event, + }, + }) } }) }) @@ -235,31 +242,23 @@ export namespace SyncEvent { }) } - export function subscribeAll(handler: (event: { def: Definition; event: Event }) => void) { - Bus.on("event", handler) - return () => Bus.off("event", handler) - } - export function payloads() { - return z - .union( - registry - .entries() - .map(([type, def]) => { - return z - .object({ - type: z.literal(type), - aggregate: z.literal(def.aggregate), - data: def.schema, - }) - .meta({ - ref: "SyncEvent" + "." + def.type, - }) + return registry + .entries() + .map(([type, def]) => { + return z + .object({ + type: z.literal("sync"), + name: z.literal(type), + id: z.string(), + seq: z.number(), + aggregateID: z.literal(def.aggregate), + data: def.schema, + }) + .meta({ + ref: "SyncEvent" + "." + def.type, }) - .toArray() as any, - ) - .meta({ - ref: "SyncEvent", }) + .toArray() } } diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index f297cb18e..b5fc976bb 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -51,7 +51,6 @@ import type { GlobalDisposeResponses, GlobalEventResponses, GlobalHealthResponses, - GlobalSyncEventSubscribeResponses, GlobalUpgradeErrors, GlobalUpgradeResponses, InstanceDisposeResponses, @@ -237,20 +236,6 @@ class HeyApiRegistry { } } -export class SyncEvent extends HeyApiClient { - /** - * Subscribe to global sync events - * - * Get global sync events - */ - public subscribe(options?: Options) { - return (options?.client ?? this.client).sse.get({ - url: "/global/sync-event", - ...options, - }) - } -} - export class Config extends HeyApiClient { /** * Get global configuration @@ -350,11 +335,6 @@ export class Global extends HeyApiClient { }) } - private _syncEvent?: SyncEvent - get syncEvent(): SyncEvent { - return (this._syncEvent ??= new SyncEvent({ client: this.client })) - } - private _config?: Config get config(): Config { return (this._config ??= new Config({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 83ec3d751..e2a9a88ad 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -971,64 +971,12 @@ export type EventSessionDeleted = { } } -export type Event = - | EventProjectUpdated - | EventServerInstanceDisposed - | EventInstallationUpdated - | EventInstallationUpdateAvailable - | EventServerConnected - | EventGlobalDisposed - | EventFileEdited - | EventFileWatcherUpdated - | EventLspClientDiagnostics - | EventLspUpdated - | EventMessagePartDelta - | EventPermissionAsked - | EventPermissionReplied - | EventSessionDiff - | EventSessionError - | EventQuestionAsked - | EventQuestionReplied - | EventQuestionRejected - | EventTodoUpdated - | EventSessionStatus - | EventSessionIdle - | EventSessionCompacted - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventMcpBrowserOpenFailed - | EventCommandExecuted - | EventVcsBranchUpdated - | EventWorktreeReady - | EventWorktreeFailed - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceStatus - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventSessionCreated - | EventSessionUpdated - | EventSessionDeleted - -export type GlobalEvent = { - directory: string - project?: string - workspace?: string - payload: Event -} - export type SyncEventMessageUpdated = { - type: "message.updated.1" - aggregate: "sessionID" + type: "sync" + name: "message.updated.1" + id: string + seq: number + aggregateID: "sessionID" data: { sessionID: string info: Message @@ -1036,8 +984,11 @@ export type SyncEventMessageUpdated = { } export type SyncEventMessageRemoved = { - type: "message.removed.1" - aggregate: "sessionID" + type: "sync" + name: "message.removed.1" + id: string + seq: number + aggregateID: "sessionID" data: { sessionID: string messageID: string @@ -1045,8 +996,11 @@ export type SyncEventMessageRemoved = { } export type SyncEventMessagePartUpdated = { - type: "message.part.updated.1" - aggregate: "sessionID" + type: "sync" + name: "message.part.updated.1" + id: string + seq: number + aggregateID: "sessionID" data: { sessionID: string part: Part @@ -1055,8 +1009,11 @@ export type SyncEventMessagePartUpdated = { } export type SyncEventMessagePartRemoved = { - type: "message.part.removed.1" - aggregate: "sessionID" + type: "sync" + name: "message.part.removed.1" + id: string + seq: number + aggregateID: "sessionID" data: { sessionID: string messageID: string @@ -1065,8 +1022,11 @@ export type SyncEventMessagePartRemoved = { } export type SyncEventSessionCreated = { - type: "session.created.1" - aggregate: "sessionID" + type: "sync" + name: "session.created.1" + id: string + seq: number + aggregateID: "sessionID" data: { sessionID: string info: Session @@ -1074,8 +1034,11 @@ export type SyncEventSessionCreated = { } export type SyncEventSessionUpdated = { - type: "session.updated.1" - aggregate: "sessionID" + type: "sync" + name: "session.updated.1" + id: string + seq: number + aggregateID: "sessionID" data: { sessionID: string info: { @@ -1114,16 +1077,75 @@ export type SyncEventSessionUpdated = { } export type SyncEventSessionDeleted = { - type: "session.deleted.1" - aggregate: "sessionID" + type: "sync" + name: "session.deleted.1" + id: string + seq: number + aggregateID: "sessionID" data: { sessionID: string info: Session } } -export type SyncEvent = { - payload: SyncEvent +export type GlobalEvent = { + directory: string + project?: string + workspace?: string + payload: + | EventProjectUpdated + | EventServerInstanceDisposed + | EventInstallationUpdated + | EventInstallationUpdateAvailable + | EventServerConnected + | EventGlobalDisposed + | EventFileEdited + | EventFileWatcherUpdated + | EventLspClientDiagnostics + | EventLspUpdated + | EventMessagePartDelta + | EventPermissionAsked + | EventPermissionReplied + | EventSessionDiff + | EventSessionError + | EventQuestionAsked + | EventQuestionReplied + | EventQuestionRejected + | EventTodoUpdated + | EventSessionStatus + | EventSessionIdle + | EventSessionCompacted + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventMcpBrowserOpenFailed + | EventCommandExecuted + | EventVcsBranchUpdated + | EventWorktreeReady + | EventWorktreeFailed + | EventPtyCreated + | EventPtyUpdated + | EventPtyExited + | EventPtyDeleted + | EventWorkspaceReady + | EventWorkspaceFailed + | EventWorkspaceStatus + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartRemoved + | EventSessionCreated + | EventSessionUpdated + | EventSessionDeleted + | SyncEventMessageUpdated + | SyncEventMessageRemoved + | SyncEventMessagePartUpdated + | SyncEventMessagePartRemoved + | SyncEventSessionCreated + | SyncEventSessionUpdated + | SyncEventSessionDeleted } /** @@ -1982,6 +2004,54 @@ export type File = { status: "added" | "deleted" | "modified" } +export type Event = + | EventProjectUpdated + | EventServerInstanceDisposed + | EventInstallationUpdated + | EventInstallationUpdateAvailable + | EventServerConnected + | EventGlobalDisposed + | EventFileEdited + | EventFileWatcherUpdated + | EventLspClientDiagnostics + | EventLspUpdated + | EventMessagePartDelta + | EventPermissionAsked + | EventPermissionReplied + | EventSessionDiff + | EventSessionError + | EventQuestionAsked + | EventQuestionReplied + | EventQuestionRejected + | EventTodoUpdated + | EventSessionStatus + | EventSessionIdle + | EventSessionCompacted + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventMcpBrowserOpenFailed + | EventCommandExecuted + | EventVcsBranchUpdated + | EventWorktreeReady + | EventWorktreeFailed + | EventPtyCreated + | EventPtyUpdated + | EventPtyExited + | EventPtyDeleted + | EventWorkspaceReady + | EventWorkspaceFailed + | EventWorkspaceStatus + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartRemoved + | EventSessionCreated + | EventSessionUpdated + | EventSessionDeleted + export type McpStatusConnected = { status: "connected" } @@ -2113,23 +2183,6 @@ export type GlobalEventResponses = { export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses] -export type GlobalSyncEventSubscribeData = { - body?: never - path?: never - query?: never - url: "/global/sync-event" -} - -export type GlobalSyncEventSubscribeResponses = { - /** - * Event stream - */ - 200: SyncEvent -} - -export type GlobalSyncEventSubscribeResponse = - GlobalSyncEventSubscribeResponses[keyof GlobalSyncEventSubscribeResponses] - export type GlobalConfigGetData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 85443726f..54f11eccf 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -66,31 +66,6 @@ ] } }, - "/global/sync-event": { - "get": { - "operationId": "global.sync-event.subscribe", - "summary": "Subscribe to global sync events", - "description": "Get global sync events", - "responses": { - "200": { - "description": "Event stream", - "content": { - "text/event-stream": { - "schema": { - "$ref": "#/components/schemas/SyncEvent" - } - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.sync-event.subscribe({\n ...\n})" - } - ] - } - }, "/global/config": { "get": { "operationId": "global.config.get", @@ -9925,174 +9900,24 @@ }, "required": ["type", "properties"] }, - "Event": { - "anyOf": [ - { - "$ref": "#/components/schemas/Event.project.updated" - }, - { - "$ref": "#/components/schemas/Event.server.instance.disposed" - }, - { - "$ref": "#/components/schemas/Event.installation.updated" - }, - { - "$ref": "#/components/schemas/Event.installation.update-available" - }, - { - "$ref": "#/components/schemas/Event.server.connected" - }, - { - "$ref": "#/components/schemas/Event.global.disposed" - }, - { - "$ref": "#/components/schemas/Event.file.edited" - }, - { - "$ref": "#/components/schemas/Event.file.watcher.updated" - }, - { - "$ref": "#/components/schemas/Event.lsp.client.diagnostics" - }, - { - "$ref": "#/components/schemas/Event.lsp.updated" - }, - { - "$ref": "#/components/schemas/Event.message.part.delta" - }, - { - "$ref": "#/components/schemas/Event.permission.asked" - }, - { - "$ref": "#/components/schemas/Event.permission.replied" - }, - { - "$ref": "#/components/schemas/Event.session.diff" - }, - { - "$ref": "#/components/schemas/Event.session.error" - }, - { - "$ref": "#/components/schemas/Event.question.asked" - }, - { - "$ref": "#/components/schemas/Event.question.replied" - }, - { - "$ref": "#/components/schemas/Event.question.rejected" - }, - { - "$ref": "#/components/schemas/Event.todo.updated" - }, - { - "$ref": "#/components/schemas/Event.session.status" - }, - { - "$ref": "#/components/schemas/Event.session.idle" - }, - { - "$ref": "#/components/schemas/Event.session.compacted" - }, - { - "$ref": "#/components/schemas/Event.tui.prompt.append" - }, - { - "$ref": "#/components/schemas/Event.tui.command.execute" - }, - { - "$ref": "#/components/schemas/Event.tui.toast.show" - }, - { - "$ref": "#/components/schemas/Event.tui.session.select" - }, - { - "$ref": "#/components/schemas/Event.mcp.tools.changed" - }, - { - "$ref": "#/components/schemas/Event.mcp.browser.open.failed" - }, - { - "$ref": "#/components/schemas/Event.command.executed" - }, - { - "$ref": "#/components/schemas/Event.vcs.branch.updated" - }, - { - "$ref": "#/components/schemas/Event.worktree.ready" - }, - { - "$ref": "#/components/schemas/Event.worktree.failed" - }, - { - "$ref": "#/components/schemas/Event.pty.created" - }, - { - "$ref": "#/components/schemas/Event.pty.updated" - }, - { - "$ref": "#/components/schemas/Event.pty.exited" - }, - { - "$ref": "#/components/schemas/Event.pty.deleted" - }, - { - "$ref": "#/components/schemas/Event.workspace.ready" - }, - { - "$ref": "#/components/schemas/Event.workspace.failed" - }, - { - "$ref": "#/components/schemas/Event.workspace.status" - }, - { - "$ref": "#/components/schemas/Event.message.updated" - }, - { - "$ref": "#/components/schemas/Event.message.removed" - }, - { - "$ref": "#/components/schemas/Event.message.part.updated" - }, - { - "$ref": "#/components/schemas/Event.message.part.removed" - }, - { - "$ref": "#/components/schemas/Event.session.created" - }, - { - "$ref": "#/components/schemas/Event.session.updated" - }, - { - "$ref": "#/components/schemas/Event.session.deleted" - } - ] - }, - "GlobalEvent": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "project": { - "type": "string" - }, - "workspace": { - "type": "string" - }, - "payload": { - "$ref": "#/components/schemas/Event" - } - }, - "required": ["directory", "payload"] - }, "SyncEvent.message.updated": { "type": "object", "properties": { "type": { + "type": "string", + "const": "sync" + }, + "name": { "type": "string", "const": "message.updated.1" }, - "aggregate": { + "id": { + "type": "string" + }, + "seq": { + "type": "number" + }, + "aggregateID": { "type": "string", "const": "sessionID" }, @@ -10110,16 +9935,26 @@ "required": ["sessionID", "info"] } }, - "required": ["type", "aggregate", "data"] + "required": ["type", "name", "id", "seq", "aggregateID", "data"] }, "SyncEvent.message.removed": { "type": "object", "properties": { "type": { + "type": "string", + "const": "sync" + }, + "name": { "type": "string", "const": "message.removed.1" }, - "aggregate": { + "id": { + "type": "string" + }, + "seq": { + "type": "number" + }, + "aggregateID": { "type": "string", "const": "sessionID" }, @@ -10138,16 +9973,26 @@ "required": ["sessionID", "messageID"] } }, - "required": ["type", "aggregate", "data"] + "required": ["type", "name", "id", "seq", "aggregateID", "data"] }, "SyncEvent.message.part.updated": { "type": "object", "properties": { "type": { + "type": "string", + "const": "sync" + }, + "name": { "type": "string", "const": "message.part.updated.1" }, - "aggregate": { + "id": { + "type": "string" + }, + "seq": { + "type": "number" + }, + "aggregateID": { "type": "string", "const": "sessionID" }, @@ -10168,16 +10013,26 @@ "required": ["sessionID", "part", "time"] } }, - "required": ["type", "aggregate", "data"] + "required": ["type", "name", "id", "seq", "aggregateID", "data"] }, "SyncEvent.message.part.removed": { "type": "object", "properties": { "type": { + "type": "string", + "const": "sync" + }, + "name": { "type": "string", "const": "message.part.removed.1" }, - "aggregate": { + "id": { + "type": "string" + }, + "seq": { + "type": "number" + }, + "aggregateID": { "type": "string", "const": "sessionID" }, @@ -10200,16 +10055,26 @@ "required": ["sessionID", "messageID", "partID"] } }, - "required": ["type", "aggregate", "data"] + "required": ["type", "name", "id", "seq", "aggregateID", "data"] }, "SyncEvent.session.created": { "type": "object", "properties": { "type": { + "type": "string", + "const": "sync" + }, + "name": { "type": "string", "const": "session.created.1" }, - "aggregate": { + "id": { + "type": "string" + }, + "seq": { + "type": "number" + }, + "aggregateID": { "type": "string", "const": "sessionID" }, @@ -10227,16 +10092,26 @@ "required": ["sessionID", "info"] } }, - "required": ["type", "aggregate", "data"] + "required": ["type", "name", "id", "seq", "aggregateID", "data"] }, "SyncEvent.session.updated": { "type": "object", "properties": { "type": { + "type": "string", + "const": "sync" + }, + "name": { "type": "string", "const": "session.updated.1" }, - "aggregate": { + "id": { + "type": "string" + }, + "seq": { + "type": "number" + }, + "aggregateID": { "type": "string", "const": "sessionID" }, @@ -10479,16 +10354,26 @@ "required": ["sessionID", "info"] } }, - "required": ["type", "aggregate", "data"] + "required": ["type", "name", "id", "seq", "aggregateID", "data"] }, "SyncEvent.session.deleted": { "type": "object", "properties": { "type": { + "type": "string", + "const": "sync" + }, + "name": { "type": "string", "const": "session.deleted.1" }, - "aggregate": { + "id": { + "type": "string" + }, + "seq": { + "type": "number" + }, + "aggregateID": { "type": "string", "const": "sessionID" }, @@ -10506,16 +10391,185 @@ "required": ["sessionID", "info"] } }, - "required": ["type", "aggregate", "data"] + "required": ["type", "name", "id", "seq", "aggregateID", "data"] }, - "SyncEvent": { + "GlobalEvent": { "type": "object", "properties": { + "directory": { + "type": "string" + }, + "project": { + "type": "string" + }, + "workspace": { + "type": "string" + }, "payload": { - "$ref": "#/components/schemas/SyncEvent" + "anyOf": [ + { + "$ref": "#/components/schemas/Event.project.updated" + }, + { + "$ref": "#/components/schemas/Event.server.instance.disposed" + }, + { + "$ref": "#/components/schemas/Event.installation.updated" + }, + { + "$ref": "#/components/schemas/Event.installation.update-available" + }, + { + "$ref": "#/components/schemas/Event.server.connected" + }, + { + "$ref": "#/components/schemas/Event.global.disposed" + }, + { + "$ref": "#/components/schemas/Event.file.edited" + }, + { + "$ref": "#/components/schemas/Event.file.watcher.updated" + }, + { + "$ref": "#/components/schemas/Event.lsp.client.diagnostics" + }, + { + "$ref": "#/components/schemas/Event.lsp.updated" + }, + { + "$ref": "#/components/schemas/Event.message.part.delta" + }, + { + "$ref": "#/components/schemas/Event.permission.asked" + }, + { + "$ref": "#/components/schemas/Event.permission.replied" + }, + { + "$ref": "#/components/schemas/Event.session.diff" + }, + { + "$ref": "#/components/schemas/Event.session.error" + }, + { + "$ref": "#/components/schemas/Event.question.asked" + }, + { + "$ref": "#/components/schemas/Event.question.replied" + }, + { + "$ref": "#/components/schemas/Event.question.rejected" + }, + { + "$ref": "#/components/schemas/Event.todo.updated" + }, + { + "$ref": "#/components/schemas/Event.session.status" + }, + { + "$ref": "#/components/schemas/Event.session.idle" + }, + { + "$ref": "#/components/schemas/Event.session.compacted" + }, + { + "$ref": "#/components/schemas/Event.tui.prompt.append" + }, + { + "$ref": "#/components/schemas/Event.tui.command.execute" + }, + { + "$ref": "#/components/schemas/Event.tui.toast.show" + }, + { + "$ref": "#/components/schemas/Event.tui.session.select" + }, + { + "$ref": "#/components/schemas/Event.mcp.tools.changed" + }, + { + "$ref": "#/components/schemas/Event.mcp.browser.open.failed" + }, + { + "$ref": "#/components/schemas/Event.command.executed" + }, + { + "$ref": "#/components/schemas/Event.vcs.branch.updated" + }, + { + "$ref": "#/components/schemas/Event.worktree.ready" + }, + { + "$ref": "#/components/schemas/Event.worktree.failed" + }, + { + "$ref": "#/components/schemas/Event.pty.created" + }, + { + "$ref": "#/components/schemas/Event.pty.updated" + }, + { + "$ref": "#/components/schemas/Event.pty.exited" + }, + { + "$ref": "#/components/schemas/Event.pty.deleted" + }, + { + "$ref": "#/components/schemas/Event.workspace.ready" + }, + { + "$ref": "#/components/schemas/Event.workspace.failed" + }, + { + "$ref": "#/components/schemas/Event.workspace.status" + }, + { + "$ref": "#/components/schemas/Event.message.updated" + }, + { + "$ref": "#/components/schemas/Event.message.removed" + }, + { + "$ref": "#/components/schemas/Event.message.part.updated" + }, + { + "$ref": "#/components/schemas/Event.message.part.removed" + }, + { + "$ref": "#/components/schemas/Event.session.created" + }, + { + "$ref": "#/components/schemas/Event.session.updated" + }, + { + "$ref": "#/components/schemas/Event.session.deleted" + }, + { + "$ref": "#/components/schemas/SyncEvent.message.updated" + }, + { + "$ref": "#/components/schemas/SyncEvent.message.removed" + }, + { + "$ref": "#/components/schemas/SyncEvent.message.part.updated" + }, + { + "$ref": "#/components/schemas/SyncEvent.message.part.removed" + }, + { + "$ref": "#/components/schemas/SyncEvent.session.created" + }, + { + "$ref": "#/components/schemas/SyncEvent.session.updated" + }, + { + "$ref": "#/components/schemas/SyncEvent.session.deleted" + } + ] } }, - "required": ["payload"] + "required": ["directory", "payload"] }, "LogLevel": { "description": "Log level", @@ -12608,6 +12662,148 @@ }, "required": ["path", "added", "removed", "status"] }, + "Event": { + "anyOf": [ + { + "$ref": "#/components/schemas/Event.project.updated" + }, + { + "$ref": "#/components/schemas/Event.server.instance.disposed" + }, + { + "$ref": "#/components/schemas/Event.installation.updated" + }, + { + "$ref": "#/components/schemas/Event.installation.update-available" + }, + { + "$ref": "#/components/schemas/Event.server.connected" + }, + { + "$ref": "#/components/schemas/Event.global.disposed" + }, + { + "$ref": "#/components/schemas/Event.file.edited" + }, + { + "$ref": "#/components/schemas/Event.file.watcher.updated" + }, + { + "$ref": "#/components/schemas/Event.lsp.client.diagnostics" + }, + { + "$ref": "#/components/schemas/Event.lsp.updated" + }, + { + "$ref": "#/components/schemas/Event.message.part.delta" + }, + { + "$ref": "#/components/schemas/Event.permission.asked" + }, + { + "$ref": "#/components/schemas/Event.permission.replied" + }, + { + "$ref": "#/components/schemas/Event.session.diff" + }, + { + "$ref": "#/components/schemas/Event.session.error" + }, + { + "$ref": "#/components/schemas/Event.question.asked" + }, + { + "$ref": "#/components/schemas/Event.question.replied" + }, + { + "$ref": "#/components/schemas/Event.question.rejected" + }, + { + "$ref": "#/components/schemas/Event.todo.updated" + }, + { + "$ref": "#/components/schemas/Event.session.status" + }, + { + "$ref": "#/components/schemas/Event.session.idle" + }, + { + "$ref": "#/components/schemas/Event.session.compacted" + }, + { + "$ref": "#/components/schemas/Event.tui.prompt.append" + }, + { + "$ref": "#/components/schemas/Event.tui.command.execute" + }, + { + "$ref": "#/components/schemas/Event.tui.toast.show" + }, + { + "$ref": "#/components/schemas/Event.tui.session.select" + }, + { + "$ref": "#/components/schemas/Event.mcp.tools.changed" + }, + { + "$ref": "#/components/schemas/Event.mcp.browser.open.failed" + }, + { + "$ref": "#/components/schemas/Event.command.executed" + }, + { + "$ref": "#/components/schemas/Event.vcs.branch.updated" + }, + { + "$ref": "#/components/schemas/Event.worktree.ready" + }, + { + "$ref": "#/components/schemas/Event.worktree.failed" + }, + { + "$ref": "#/components/schemas/Event.pty.created" + }, + { + "$ref": "#/components/schemas/Event.pty.updated" + }, + { + "$ref": "#/components/schemas/Event.pty.exited" + }, + { + "$ref": "#/components/schemas/Event.pty.deleted" + }, + { + "$ref": "#/components/schemas/Event.workspace.ready" + }, + { + "$ref": "#/components/schemas/Event.workspace.failed" + }, + { + "$ref": "#/components/schemas/Event.workspace.status" + }, + { + "$ref": "#/components/schemas/Event.message.updated" + }, + { + "$ref": "#/components/schemas/Event.message.removed" + }, + { + "$ref": "#/components/schemas/Event.message.part.updated" + }, + { + "$ref": "#/components/schemas/Event.message.part.removed" + }, + { + "$ref": "#/components/schemas/Event.session.created" + }, + { + "$ref": "#/components/schemas/Event.session.updated" + }, + { + "$ref": "#/components/schemas/Event.session.deleted" + } + ] + }, "MCPStatusConnected": { "type": "object", "properties": { -- cgit v1.2.3 From 85674f4bfd15723c6c8afd96d74120ded9b8437b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 14 Apr 2026 19:45:10 +0000 Subject: chore: generate --- packages/sdk/js/src/v2/gen/sdk.gen.ts | 6 +- packages/sdk/js/src/v2/gen/types.gen.ts | 46 +++++++------ packages/sdk/openapi.json | 117 +++++++++++++++++--------------- 3 files changed, 91 insertions(+), 78 deletions(-) (limited to 'packages/sdk/openapi.json') diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index b5fc976bb..cbd8b59f2 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -105,10 +105,10 @@ import type { PtyRemoveResponses, PtyUpdateErrors, PtyUpdateResponses, - QuestionAnswer, QuestionListResponses, QuestionRejectErrors, QuestionRejectResponses, + QuestionReply, QuestionReplyErrors, QuestionReplyResponses, SessionAbortErrors, @@ -2738,7 +2738,7 @@ export class Question extends HeyApiClient { requestID: string directory?: string workspace?: string - answers?: Array + questionReply?: QuestionReply }, options?: Options, ) { @@ -2750,7 +2750,7 @@ export class Question extends HeyApiClient { { in: "path", key: "requestID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "answers" }, + { key: "questionReply", map: "body" }, ], }, ], diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index e2a9a88ad..141c93c52 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -264,6 +264,11 @@ export type QuestionInfo = { custom?: boolean } +export type QuestionTool = { + messageID: string + callID: string +} + export type QuestionRequest = { id: string sessionID: string @@ -271,10 +276,7 @@ export type QuestionRequest = { * Questions to ask */ questions: Array - tool?: { - messageID: string - callID: string - } + tool?: QuestionTool } export type EventQuestionAsked = { @@ -284,21 +286,25 @@ export type EventQuestionAsked = { export type QuestionAnswer = Array +export type QuestionReplied = { + sessionID: string + requestID: string + answers: Array +} + export type EventQuestionReplied = { type: "question.replied" - properties: { - sessionID: string - requestID: string - answers: Array - } + properties: QuestionReplied +} + +export type QuestionRejected = { + sessionID: string + requestID: string } export type EventQuestionRejected = { type: "question.rejected" - properties: { - sessionID: string - requestID: string - } + properties: QuestionRejected } export type Todo = { @@ -1919,6 +1925,13 @@ export type SubtaskPartInput = { command?: string } +export type QuestionReply = { + /** + * User answers in order of questions (each answer is an array of selected labels) + */ + answers: Array +} + export type ProviderAuthMethod = { type: "oauth" | "api" label: string @@ -4246,12 +4259,7 @@ export type QuestionListResponses = { export type QuestionListResponse = QuestionListResponses[keyof QuestionListResponses] export type QuestionReplyData = { - body?: { - /** - * User answers in order of questions (each answer is an array of selected labels) - */ - answers: Array - } + body?: QuestionReply path: { requestID: string } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 54f11eccf..e0233b152 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -4769,17 +4769,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "answers": { - "description": "User answers in order of questions (each answer is an array of selected labels)", - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": ["answers"] + "$ref": "#/components/schemas/QuestionReply" } } } @@ -7859,16 +7849,26 @@ }, "required": ["question", "header", "options"] }, + "QuestionTool": { + "type": "object", + "properties": { + "messageID": { + "type": "string" + }, + "callID": { + "type": "string" + } + }, + "required": ["messageID", "callID"] + }, "QuestionRequest": { "type": "object", "properties": { "id": { - "type": "string", - "pattern": "^que.*" + "type": "string" }, "sessionID": { - "type": "string", - "pattern": "^ses.*" + "type": "string" }, "questions": { "description": "Questions to ask", @@ -7878,17 +7878,7 @@ } }, "tool": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg.*" - }, - "callID": { - "type": "string" - } - }, - "required": ["messageID", "callID"] + "$ref": "#/components/schemas/QuestionTool" } }, "required": ["id", "sessionID", "questions"] @@ -7912,6 +7902,24 @@ "type": "string" } }, + "QuestionReplied": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "requestID": { + "type": "string" + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionAnswer" + } + } + }, + "required": ["sessionID", "requestID", "answers"] + }, "Event.question.replied": { "type": "object", "properties": { @@ -7920,28 +7928,23 @@ "const": "question.replied" }, "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses.*" - }, - "requestID": { - "type": "string", - "pattern": "^que.*" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": ["sessionID", "requestID", "answers"] + "$ref": "#/components/schemas/QuestionReplied" } }, "required": ["type", "properties"] }, + "QuestionRejected": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "requestID": { + "type": "string" + } + }, + "required": ["sessionID", "requestID"] + }, "Event.question.rejected": { "type": "object", "properties": { @@ -7950,18 +7953,7 @@ "const": "question.rejected" }, "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses.*" - }, - "requestID": { - "type": "string", - "pattern": "^que.*" - } - }, - "required": ["sessionID", "requestID"] + "$ref": "#/components/schemas/QuestionRejected" } }, "required": ["type", "properties"] @@ -12369,6 +12361,19 @@ }, "required": ["type", "prompt", "description", "agent"] }, + "QuestionReply": { + "type": "object", + "properties": { + "answers": { + "description": "User answers in order of questions (each answer is an array of selected labels)", + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionAnswer" + } + } + }, + "required": ["answers"] + }, "ProviderAuthMethod": { "type": "object", "properties": { -- cgit v1.2.3 From 4ed3afea84b13b400f8e4304d673ff8bd751e469 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 14 Apr 2026 20:58:35 +0000 Subject: chore: generate --- packages/sdk/openapi.json | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) (limited to 'packages/sdk/openapi.json') diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index e0233b152..6000e6604 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -4769,7 +4769,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QuestionReply" + "type": "object", + "properties": { + "answers": { + "description": "User answers in order of questions (each answer is an array of selected labels)", + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionAnswer" + } + } + }, + "required": ["answers"] } } } @@ -12361,19 +12371,6 @@ }, "required": ["type", "prompt", "description", "agent"] }, - "QuestionReply": { - "type": "object", - "properties": { - "answers": { - "description": "User answers in order of questions (each answer is an array of selected labels)", - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": ["answers"] - }, "ProviderAuthMethod": { "type": "object", "properties": { -- cgit v1.2.3 From af20191d1cd60a7f4a421ad81eca5053f7deace1 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 15 Apr 2026 10:18:48 -0400 Subject: feat(core): sync routes, refactor proxy, session restore, and more syncing (#22518) --- packages/opencode/src/control-plane/workspace.ts | 258 +++++++++++++++-- packages/opencode/src/server/instance/index.ts | 2 + .../opencode/src/server/instance/middleware.ts | 66 +++-- packages/opencode/src/server/instance/sync.ts | 118 ++++++++ packages/opencode/src/server/instance/workspace.ts | 71 ++++- packages/opencode/src/server/middleware.ts | 2 +- packages/opencode/src/server/proxy.ts | 55 +++- packages/opencode/src/sync/index.ts | 19 ++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 157 ++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 127 ++++++++ packages/sdk/openapi.json | 320 +++++++++++++++++++++ 11 files changed, 1132 insertions(+), 63 deletions(-) create mode 100644 packages/opencode/src/server/instance/sync.ts (limited to 'packages/sdk/openapi.json') diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index f330e07b7..78f3d770e 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -1,11 +1,13 @@ import z from "zod" import { setTimeout as sleep } from "node:timers/promises" import { fn } from "@/util/fn" -import { Database, eq } from "@/storage/db" +import { Database, asc, eq } from "@/storage/db" import { Project } from "@/project/project" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" import { SyncEvent } from "@/sync" +import { EventTable } from "@/sync/event.sql" +import { Flag } from "@/flag/flag" import { Log } from "@/util/log" import { Filesystem } from "@/util/filesystem" import { ProjectID } from "@/project/schema" @@ -15,6 +17,11 @@ import { getAdaptor } from "./adaptors" import { WorkspaceInfo } from "./types" import { WorkspaceID } from "./schema" import { parseSSE } from "./sse" +import { Session } from "@/session" +import { SessionTable } from "@/session/session.sql" +import { SessionID } from "@/session/schema" +import { errorData } from "@/util/error" +import { AppRuntime } from "@/effect/app-runtime" export namespace Workspace { export const Info = WorkspaceInfo.meta({ @@ -29,6 +36,13 @@ export namespace Workspace { }) export type ConnectionStatus = z.infer + const Restore = z.object({ + workspaceID: WorkspaceID.zod, + sessionID: SessionID.zod, + total: z.number().int().min(0), + step: z.number().int().min(0), + }) + export const Event = { Ready: BusEvent.define( "workspace.ready", @@ -42,6 +56,7 @@ export namespace Workspace { message: z.string(), }), ), + Restore: BusEvent.define("workspace.restore", Restore), Status: BusEvent.define("workspace.status", ConnectionStatus), } @@ -102,11 +117,170 @@ export namespace Workspace { return info }) + const SessionRestoreInput = z.object({ + workspaceID: WorkspaceID.zod, + sessionID: SessionID.zod, + }) + + export const sessionRestore = fn(SessionRestoreInput, async (input) => { + log.info("session restore requested", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + }) + try { + const space = await get(input.workspaceID) + if (!space) throw new Error(`Workspace not found: ${input.workspaceID}`) + + const adaptor = await getAdaptor(space.projectID, space.type) + const target = await adaptor.target(space) + + // Need to switch the workspace of the session + SyncEvent.run(Session.Event.Updated, { + sessionID: input.sessionID, + info: { + workspaceID: input.workspaceID, + }, + }) + + const rows = Database.use((db) => + db + .select({ + id: EventTable.id, + aggregateID: EventTable.aggregate_id, + seq: EventTable.seq, + type: EventTable.type, + data: EventTable.data, + }) + .from(EventTable) + .where(eq(EventTable.aggregate_id, input.sessionID)) + .orderBy(asc(EventTable.seq)) + .all(), + ) + if (rows.length === 0) throw new Error(`No events found for session: ${input.sessionID}`) + + const all = rows + + const size = 10 + const sets = Array.from({ length: Math.ceil(all.length / size) }, (_, i) => all.slice(i * size, (i + 1) * size)) + const total = sets.length + log.info("session restore prepared", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + workspaceType: space.type, + directory: space.directory, + target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory, + events: all.length, + batches: total, + first: all[0]?.seq, + last: all.at(-1)?.seq, + }) + GlobalBus.emit("event", { + directory: "global", + workspace: input.workspaceID, + payload: { + type: Event.Restore.type, + properties: { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + total, + step: 0, + }, + }, + }) + for (const [i, events] of sets.entries()) { + log.info("session restore batch starting", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + events: events.length, + first: events[0]?.seq, + last: events.at(-1)?.seq, + target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory, + }) + if (target.type === "local") { + SyncEvent.replayAll(events) + log.info("session restore batch replayed locally", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + events: events.length, + }) + } else { + const url = route(target.url, "/sync/replay") + const headers = new Headers(target.headers) + headers.set("content-type", "application/json") + const res = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify({ + directory: space.directory ?? "", + events, + }), + }) + if (!res.ok) { + const body = await res.text() + log.error("session restore batch failed", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + status: res.status, + body, + }) + throw new Error( + `Failed to replay session ${input.sessionID} into workspace ${input.workspaceID}: HTTP ${res.status} ${body}`, + ) + } + log.info("session restore batch posted", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + status: res.status, + }) + } + GlobalBus.emit("event", { + directory: "global", + workspace: input.workspaceID, + payload: { + type: Event.Restore.type, + properties: { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + total, + step: i + 1, + }, + }, + }) + } + + log.info("session restore complete", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + batches: total, + }) + + return { + total, + } + } catch (err) { + log.error("session restore failed", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + error: errorData(err), + }) + throw err + } + }) + export function list(project: Project.Info) { const rows = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(), ) const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id)) + for (const space of spaces) startSync(space) return spaces } @@ -120,13 +294,25 @@ export namespace Workspace { }) export const remove = fn(WorkspaceID.zod, async (id) => { + const sessions = Database.use((db) => + db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, id)).all(), + ) + for (const session of sessions) { + await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(session.id))) + } + const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get()) + if (row) { stopSync(id) const info = fromRow(row) - const adaptor = await getAdaptor(info.projectID, row.type) - adaptor.remove(info) + try { + const adaptor = await getAdaptor(info.projectID, row.type) + await adaptor.remove(info) + } catch (err) { + log.error("adaptor not available when removing workspace", { type: row.type }) + } Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run()) return info } @@ -156,51 +342,81 @@ export namespace Workspace { const log = Log.create({ service: "workspace-sync" }) - async function workspaceEventLoop(space: Info, signal: AbortSignal) { - log.info("starting sync: " + space.id) + function route(url: string | URL, path: string) { + const next = new URL(url) + next.pathname = `${next.pathname.replace(/\/$/, "")}${path}` + next.search = "" + next.hash = "" + return next + } + async function syncWorkspace(space: Info, signal: AbortSignal) { while (!signal.aborted) { - log.info("connecting to sync: " + space.id) + log.info("connecting to global sync", { workspace: space.name }) - setStatus(space.id, "connecting") const adaptor = await getAdaptor(space.projectID, space.type) const target = await adaptor.target(space) if (target.type === "local") return - const res = await fetch(target.url + "/sync/event", { method: "GET", signal }).catch((err: unknown) => { - setStatus(space.id, "error", String(err)) + const res = await fetch(route(target.url, "/global/event"), { + method: "GET", + headers: target.headers, + signal, + }).catch((err: unknown) => { + setStatus(space.id, "error") + + log.info("failed to connect to global sync", { + workspace: space.name, + error: err, + }) return undefined }) - if (!res || !res.ok || !res.body) { - log.info("failed to connect to sync: " + res?.status) - setStatus(space.id, "error", res ? `HTTP ${res.status}` : "no response") + if (!res || !res.ok || !res.body) { + log.info("failed to connect to global sync", { workspace: space.name }) + setStatus(space.id, "error") await sleep(1000) continue } + + log.info("global sync connected", { workspace: space.name }) setStatus(space.id, "connected") - await parseSSE(res.body, signal, (evt) => { - const event = evt as SyncEvent.SerializedEvent + await parseSSE(res.body, signal, (evt: any) => { try { - if (!event.type.startsWith("server.")) { - SyncEvent.replay(event) + if (!("payload" in evt)) return + + if (evt.payload.type === "sync") { + // This name -> type is temporary + SyncEvent.replay({ ...evt.payload, type: evt.payload.name } as SyncEvent.SerializedEvent) } + + GlobalBus.emit("event", { + directory: evt.directory, + project: evt.project, + workspace: space.id, + payload: evt.payload, + }) } catch (err) { - log.warn("failed to replay sync event", { + log.info("failed to replay global event", { workspaceID: space.id, error: err, }) } }) + + log.info("disconnected from global sync: " + space.id) setStatus(space.id, "disconnected") - log.info("disconnected to sync: " + space.id) - await sleep(250) + + // TODO: Implement exponential backoff + await sleep(1000) } } function startSync(space: Info) { + if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return + if (space.type === "worktree") { void Filesystem.exists(space.directory!).then((exists) => { setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist") @@ -213,9 +429,9 @@ export namespace Workspace { aborts.set(space.id, abort) setStatus(space.id, "disconnected") - void workspaceEventLoop(space, abort.signal).catch((error) => { + void syncWorkspace(space, abort.signal).catch((error) => { setStatus(space.id, "error", String(error)) - log.warn("workspace sync listener failed", { + log.warn("workspace listener failed", { workspaceID: space.id, error, }) diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/instance/index.ts index 86a18dc67..4a03b7b29 100644 --- a/packages/opencode/src/server/instance/index.ts +++ b/packages/opencode/src/server/instance/index.ts @@ -23,6 +23,7 @@ import { ConfigRoutes } from "./config" import { ExperimentalRoutes } from "./experimental" import { ProviderRoutes } from "./provider" import { EventRoutes } from "./event" +import { SyncRoutes } from "./sync" import { WorkspaceRouterMiddleware } from "./middleware" import { AppRuntime } from "@/effect/app-runtime" @@ -37,6 +38,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => .route("/permission", PermissionRoutes()) .route("/question", QuestionRoutes()) .route("/provider", ProviderRoutes()) + .route("/sync", SyncRoutes()) .route("/", FileRoutes()) .route("/", EventRoutes()) .route("/mcp", McpRoutes()) diff --git a/packages/opencode/src/server/instance/middleware.ts b/packages/opencode/src/server/instance/middleware.ts index 9155ad451..824c265ef 100644 --- a/packages/opencode/src/server/instance/middleware.ts +++ b/packages/opencode/src/server/instance/middleware.ts @@ -11,9 +11,12 @@ import { Session } from "@/session" import { SessionID } from "@/session/schema" import { WorkspaceContext } from "@/control-plane/workspace-context" import { AppRuntime } from "@/effect/app-runtime" +import { Log } from "@/util/log" type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" } +const OPENCODE_WORKSPACE = process.env.OPENCODE_WORKSPACE + const RULES: Array = [ { path: "/session/status", action: "forward" }, { method: "GET", path: "/session", action: "local" }, @@ -46,6 +49,8 @@ async function getSessionWorkspace(url: URL) { } export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler { + const log = Log.create({ service: "workspace-router" }) + return async (c, next) => { const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() const directory = Filesystem.resolve( @@ -63,8 +68,22 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware const sessionWorkspaceID = await getSessionWorkspace(url) const workspaceID = sessionWorkspaceID || url.searchParams.get("workspace") - // If no workspace is provided we use the project - if (!workspaceID) { + if (!workspaceID || url.pathname.startsWith("/console") || OPENCODE_WORKSPACE) { + if (OPENCODE_WORKSPACE) { + return WorkspaceContext.provide({ + workspaceID: WorkspaceID.make(OPENCODE_WORKSPACE), + async fn() { + return Instance.provide({ + directory, + init: () => AppRuntime.runPromise(InstanceBootstrap), + async fn() { + return next() + }, + }) + }, + }) + } + return Instance.provide({ directory, init: () => AppRuntime.runPromise(InstanceBootstrap), @@ -77,16 +96,6 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware const workspace = await Workspace.get(WorkspaceID.make(workspaceID)) if (!workspace) { - // Special-case deleting a session in case user's data in a - // weird state. Allow them to forcefully delete a synced session - // even if the remote workspace is not in their data. - // - // The lets the `DELETE /session/:id` endpoint through and we've - // made sure that it will run without an instance - if (url.pathname.match(/\/session\/[^/]+$/) && c.req.method === "DELETE") { - return next() - } - return new Response(`Workspace not found: ${workspaceID}`, { status: 500, headers: { @@ -95,6 +104,12 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware }) } + if (local(c.req.method, url.pathname)) { + // No instance provided because we are serving cached data; there + // is no instance to work with + return next() + } + const adaptor = await getAdaptor(workspace.projectID, workspace.type) const target = await adaptor.target(workspace) @@ -112,24 +127,27 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware }) } - if (local(c.req.method, url.pathname)) { - // No instance provided because we are serving cached data; there - // is no instance to work with - return next() - } + const proxyURL = new URL(target.url) + proxyURL.pathname = `${proxyURL.pathname.replace(/\/$/, "")}${url.pathname}` + proxyURL.search = url.search + proxyURL.hash = url.hash + proxyURL.searchParams.delete("workspace") + + log.info("workspace proxy forwarding", { + workspaceID, + request: url.toString(), + target: String(target.url), + proxy: proxyURL.toString(), + }) if (c.req.header("upgrade")?.toLowerCase() === "websocket") { - return ServerProxy.websocket(upgrade, target, c.req.raw, c.env) + return ServerProxy.websocket(upgrade, proxyURL, target.headers, c.req.raw, c.env) } const headers = new Headers(c.req.raw.headers) headers.delete("x-opencode-workspace") - return ServerProxy.http( - target, - new Request(c.req.raw, { - headers, - }), - ) + const req = new Request(c.req.raw, { headers }) + return ServerProxy.http(proxyURL, target.headers, req) } } diff --git a/packages/opencode/src/server/instance/sync.ts b/packages/opencode/src/server/instance/sync.ts new file mode 100644 index 000000000..c22969130 --- /dev/null +++ b/packages/opencode/src/server/instance/sync.ts @@ -0,0 +1,118 @@ +import z from "zod" +import { Hono } from "hono" +import { describeRoute, validator, resolver } from "hono-openapi" +import { SyncEvent } from "@/sync" +import { Database, asc, and, not, or, lte, eq } from "@/storage/db" +import { EventTable } from "@/sync/event.sql" +import { lazy } from "@/util/lazy" +import { Log } from "@/util/log" +import { errors } from "../error" + +const ReplayEvent = z.object({ + id: z.string(), + aggregateID: z.string(), + seq: z.number().int().min(0), + type: z.string(), + data: z.record(z.string(), z.unknown()), +}) + +const log = Log.create({ service: "server.sync" }) + +export const SyncRoutes = lazy(() => + new Hono() + .post( + "/replay", + describeRoute({ + summary: "Replay sync events", + description: "Validate and replay a complete sync event history.", + operationId: "sync.replay", + responses: { + 200: { + description: "Replayed sync events", + content: { + "application/json": { + schema: resolver( + z.object({ + sessionID: z.string(), + }), + ), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "json", + z.object({ + directory: z.string(), + events: z.array(ReplayEvent).min(1), + }), + ), + async (c) => { + const body = c.req.valid("json") + const events = body.events + const source = events[0].aggregateID + log.info("sync replay requested", { + sessionID: source, + events: events.length, + first: events[0]?.seq, + last: events.at(-1)?.seq, + directory: body.directory, + }) + SyncEvent.replayAll(events) + + log.info("sync replay complete", { + sessionID: source, + events: events.length, + first: events[0]?.seq, + last: events.at(-1)?.seq, + }) + + return c.json({ + sessionID: source, + }) + }, + ) + .get( + "/history", + describeRoute({ + summary: "List sync events", + description: + "List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history.", + operationId: "sync.history.list", + responses: { + 200: { + description: "Sync events", + content: { + "application/json": { + schema: resolver( + z.array( + z.object({ + id: z.string(), + aggregate_id: z.string(), + seq: z.number(), + type: z.string(), + data: z.record(z.string(), z.unknown()), + }), + ), + ), + }, + }, + }, + ...errors(400), + }, + }), + validator("json", z.record(z.string(), z.number().int().min(0))), + async (c) => { + const body = c.req.valid("json") + const exclude = Object.entries(body) + const where = + exclude.length > 0 + ? not(or(...exclude.map(([id, seq]) => and(eq(EventTable.aggregate_id, id), lte(EventTable.seq, seq))))!) + : undefined + const rows = Database.use((db) => db.select().from(EventTable).where(where).orderBy(asc(EventTable.seq)).all()) + return c.json(rows) + }, + ), +) diff --git a/packages/opencode/src/server/instance/workspace.ts b/packages/opencode/src/server/instance/workspace.ts index 7cee03197..a4ff4eda8 100644 --- a/packages/opencode/src/server/instance/workspace.ts +++ b/packages/opencode/src/server/instance/workspace.ts @@ -6,12 +6,10 @@ import { Workspace } from "../../control-plane/workspace" import { Instance } from "../../project/instance" import { errors } from "../error" import { lazy } from "../../util/lazy" +import { Log } from "@/util/log" +import { errorData } from "@/util/error" -const WorkspaceAdaptor = z.object({ - type: z.string(), - name: z.string(), - description: z.string(), -}) +const log = Log.create({ service: "server.workspace" }) export const WorkspaceRoutes = lazy(() => new Hono() @@ -26,7 +24,15 @@ export const WorkspaceRoutes = lazy(() => description: "Workspace adaptors", content: { "application/json": { - schema: resolver(z.array(WorkspaceAdaptor)), + schema: resolver( + z.array( + z.object({ + type: z.string(), + name: z.string(), + description: z.string(), + }), + ), + ), }, }, }, @@ -140,5 +146,58 @@ export const WorkspaceRoutes = lazy(() => const { id } = c.req.valid("param") return c.json(await Workspace.remove(id)) }, + ) + .post( + "/:id/session-restore", + describeRoute({ + summary: "Restore session into workspace", + description: "Replay a session's sync events into the target workspace in batches.", + operationId: "experimental.workspace.sessionRestore", + responses: { + 200: { + description: "Session replay started", + content: { + "application/json": { + schema: resolver( + z.object({ + total: z.number().int().min(0), + }), + ), + }, + }, + }, + ...errors(400), + }, + }), + validator("param", z.object({ id: Workspace.Info.shape.id })), + validator("json", Workspace.sessionRestore.schema.omit({ workspaceID: true })), + async (c) => { + const { id } = c.req.valid("param") + const body = c.req.valid("json") + log.info("session restore route requested", { + workspaceID: id, + sessionID: body.sessionID, + directory: Instance.directory, + }) + try { + const result = await Workspace.sessionRestore({ + workspaceID: id, + ...body, + }) + log.info("session restore route complete", { + workspaceID: id, + sessionID: body.sessionID, + total: result.total, + }) + return c.json(result) + } catch (err) { + log.error("session restore route failed", { + workspaceID: id, + sessionID: body.sessionID, + error: errorData(err), + }) + throw err + } + }, ), ) diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts index a51ba602b..d0539eb24 100644 --- a/packages/opencode/src/server/middleware.ts +++ b/packages/opencode/src/server/middleware.ts @@ -86,7 +86,7 @@ const zipped = compress() export const CompressionMiddleware: MiddlewareHandler = (c, next) => { const path = c.req.path const method = c.req.method - if (path === "/event" || path === "/global/event" || path === "/global/sync-event") return next() + if (path === "/event" || path === "/global/event") return next() if (method === "POST" && /\/session\/[^/]+\/(message|prompt_async)$/.test(path)) return next() return zipped(c, next) } diff --git a/packages/opencode/src/server/proxy.ts b/packages/opencode/src/server/proxy.ts index c90a657dc..0c0deba20 100644 --- a/packages/opencode/src/server/proxy.ts +++ b/packages/opencode/src/server/proxy.ts @@ -1,6 +1,6 @@ -import type { Target } from "@/control-plane/types" import { Hono } from "hono" import type { UpgradeWebSocket } from "hono/ws" +import { Log } from "@/util/log" const hop = new Set([ "connection", @@ -20,6 +20,7 @@ type Msg = string | ArrayBuffer | Uint8Array function headers(req: Request, extra?: HeadersInit) { const out = new Headers(req.headers) for (const key of hop) out.delete(key) + out.delete("accept-encoding") out.delete("x-opencode-directory") out.delete("x-opencode-workspace") if (!extra) return out @@ -98,31 +99,63 @@ const app = (upgrade: UpgradeWebSocket) => ) export namespace ServerProxy { - export function http(target: Extract, req: Request) { + const log = Log.Default.clone().tag("service", "server-proxy") + + export function http(url: string | URL, extra: HeadersInit | undefined, req: Request) { + console.log("proxy http request", { + method: req.method, + request: req.url, + url: String(url), + }) return fetch( - new Request(target.url, { + new Request(url, { method: req.method, - headers: headers(req, target.headers), + headers: headers(req, extra), body: req.method === "GET" || req.method === "HEAD" ? undefined : req.body, redirect: "manual", signal: req.signal, }), - ) + ).then((res) => { + const next = new Headers(res.headers) + next.delete("content-encoding") + next.delete("content-length") + + console.log("proxy http response", { + method: req.method, + request: req.url, + url: String(url), + status: res.status, + statusText: res.statusText, + }) + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: next, + }) + }) } export function websocket( upgrade: UpgradeWebSocket, - target: Extract, + target: string | URL, + extra: HeadersInit | undefined, req: Request, env: unknown, ) { - const url = new URL(req.url) - url.pathname = "/__workspace_ws" - url.search = "" + const proxy = new URL(req.url) + proxy.pathname = "/__workspace_ws" + proxy.search = "" const next = new Headers(req.headers) - next.set("x-opencode-proxy-url", socket(target.url)) + next.set("x-opencode-proxy-url", socket(target)) + for (const [key, value] of new Headers(extra).entries()) { + next.set(key, value) + } + log.info("proxy websocket", { + request: req.url, + target: String(target), + }) return app(upgrade).fetch( - new Request(url, { + new Request(proxy, { method: req.method, headers: next, signal: req.signal, diff --git a/packages/opencode/src/sync/index.ts b/packages/opencode/src/sync/index.ts index d7cb7f774..ce598dae6 100644 --- a/packages/opencode/src/sync/index.ts +++ b/packages/opencode/src/sync/index.ts @@ -199,6 +199,25 @@ export namespace SyncEvent { process(def, event, { publish: !!options?.publish }) } + export function replayAll(events: SerializedEvent[], options?: { publish: boolean }) { + const source = events[0]?.aggregateID + if (!source) return + if (events.some((item) => item.aggregateID !== source)) { + throw new Error("Replay events must belong to the same session") + } + const start = events[0].seq + for (const [i, item] of events.entries()) { + const seq = start + i + if (item.seq !== seq) { + throw new Error(`Replay sequence mismatch at index ${i}: expected ${seq}, got ${item.seq}`) + } + } + for (const item of events) { + replay(item, options) + } + return source + } + export function run(def: Def, data: Event["data"], options?: { publish?: boolean }) { const agg = (data as Record)[def.aggregate] // This should never happen: we've enforced it via typescript in diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index b5fc976bb..d7bf43f50 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -35,6 +35,8 @@ import type { ExperimentalWorkspaceListResponses, ExperimentalWorkspaceRemoveErrors, ExperimentalWorkspaceRemoveResponses, + ExperimentalWorkspaceSessionRestoreErrors, + ExperimentalWorkspaceSessionRestoreResponses, ExperimentalWorkspaceStatusResponses, FileListResponses, FilePartInput, @@ -157,6 +159,10 @@ import type { SessionUpdateErrors, SessionUpdateResponses, SubtaskPartInput, + SyncHistoryListErrors, + SyncHistoryListResponses, + SyncReplayErrors, + SyncReplayResponses, TextPartInput, ToolIdsErrors, ToolIdsResponses, @@ -1243,6 +1249,49 @@ export class Workspace extends HeyApiClient { }) } + /** + * Restore session into workspace + * + * Replay a session's sync events into the target workspace in batches. + */ + public sessionRestore( + parameters: { + id: string + directory?: string + workspace?: string + sessionID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "id" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "sessionID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalWorkspaceSessionRestoreResponses, + ExperimentalWorkspaceSessionRestoreErrors, + ThrowOnError + >({ + url: "/experimental/workspace/{id}/session-restore", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + private _adaptor?: Adaptor get adaptor(): Adaptor { return (this._adaptor ??= new Adaptor({ client: this.client })) @@ -2961,6 +3010,109 @@ export class Provider extends HeyApiClient { } } +export class History extends HeyApiClient { + /** + * List sync events + * + * List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history. + */ + public list( + parameters?: { + directory?: string + workspace?: string + body?: { + [key: string]: number + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "body", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/sync/history", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Sync extends HeyApiClient { + /** + * Replay sync events + * + * Validate and replay a complete sync event history. + */ + public replay( + parameters?: { + query_directory?: string + workspace?: string + body_directory?: string + events?: Array<{ + id: string + aggregateID: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { + in: "query", + key: "query_directory", + map: "directory", + }, + { in: "query", key: "workspace" }, + { + in: "body", + key: "body_directory", + map: "directory", + }, + { in: "body", key: "events" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/sync/replay", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + private _history?: History + get history(): History { + return (this._history ??= new History({ client: this.client })) + } +} + export class Find extends HeyApiClient { /** * Find text @@ -4217,6 +4369,11 @@ export class OpencodeClient extends HeyApiClient { return (this._provider ??= new Provider({ client: this.client })) } + private _sync?: Sync + get sync(): Sync { + return (this._sync ??= new Sync({ client: this.client })) + } + private _find?: Find get find(): Find { return (this._find ??= new Find({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 8f4c16c5b..24c1d53bf 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -520,6 +520,16 @@ export type EventWorkspaceFailed = { } } +export type EventWorkspaceRestore = { + type: "workspace.restore" + properties: { + workspaceID: string + sessionID: string + total: number + step: number + } +} + export type EventWorkspaceStatus = { type: "workspace.status" properties: { @@ -1137,6 +1147,7 @@ export type GlobalEvent = { | EventPtyDeleted | EventWorkspaceReady | EventWorkspaceFailed + | EventWorkspaceRestore | EventWorkspaceStatus | EventMessageUpdated | EventMessageRemoved @@ -2049,6 +2060,7 @@ export type Event = | EventPtyDeleted | EventWorkspaceReady | EventWorkspaceFailed + | EventWorkspaceRestore | EventWorkspaceStatus | EventMessageUpdated | EventMessageRemoved @@ -3006,6 +3018,42 @@ export type ExperimentalWorkspaceRemoveResponses = { export type ExperimentalWorkspaceRemoveResponse = ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses] +export type ExperimentalWorkspaceSessionRestoreData = { + body?: { + sessionID: string + } + path: { + id: string + } + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/{id}/session-restore" +} + +export type ExperimentalWorkspaceSessionRestoreErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceSessionRestoreError = + ExperimentalWorkspaceSessionRestoreErrors[keyof ExperimentalWorkspaceSessionRestoreErrors] + +export type ExperimentalWorkspaceSessionRestoreResponses = { + /** + * Session replay started + */ + 200: { + total: number + } +} + +export type ExperimentalWorkspaceSessionRestoreResponse = + ExperimentalWorkspaceSessionRestoreResponses[keyof ExperimentalWorkspaceSessionRestoreResponses] + export type WorktreeRemoveData = { body?: WorktreeRemoveInput path?: never @@ -4456,6 +4504,85 @@ export type ProviderOauthCallbackResponses = { export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses] +export type SyncReplayData = { + body?: { + directory: string + events: Array<{ + id: string + aggregateID: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/sync/replay" +} + +export type SyncReplayErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SyncReplayError = SyncReplayErrors[keyof SyncReplayErrors] + +export type SyncReplayResponses = { + /** + * Replayed sync events + */ + 200: { + sessionID: string + } +} + +export type SyncReplayResponse = SyncReplayResponses[keyof SyncReplayResponses] + +export type SyncHistoryListData = { + body?: { + [key: string]: number + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/sync/history" +} + +export type SyncHistoryListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SyncHistoryListError = SyncHistoryListErrors[keyof SyncHistoryListErrors] + +export type SyncHistoryListResponses = { + /** + * Sync events + */ + 200: Array<{ + id: string + aggregate_id: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> +} + +export type SyncHistoryListResponse = SyncHistoryListResponses[keyof SyncHistoryListResponses] + export type FindTextData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 6000e6604..ee3538d55 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -1805,6 +1805,90 @@ ] } }, + "/experimental/workspace/{id}/session-restore": { + "post": { + "operationId": "experimental.workspace.sessionRestore", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "id", + "schema": { + "type": "string", + "pattern": "^wrk.*" + }, + "required": true + } + ], + "summary": "Restore session into workspace", + "description": "Replay a session's sync events into the target workspace in batches.", + "responses": { + "200": { + "description": "Session replay started", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["total"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses.*" + } + }, + "required": ["sessionID"] + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.sessionRestore({\n ...\n})" + } + ] + } + }, "/experimental/worktree": { "post": { "operationId": "worktree.create", @@ -5143,6 +5227,202 @@ ] } }, + "/sync/replay": { + "post": { + "operationId": "sync.replay", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "Replay sync events", + "description": "Validate and replay a complete sync event history.", + "responses": { + "200": { + "description": "Replayed sync events", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + } + }, + "required": ["sessionID"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "events": { + "minItems": 1, + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "type": { + "type": "string" + }, + "data": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["id", "aggregateID", "seq", "type", "data"] + } + } + }, + "required": ["directory", "events"] + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.replay({\n ...\n})" + } + ] + } + }, + "/sync/history": { + "get": { + "operationId": "sync.history.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "List sync events", + "description": "List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history.", + "responses": { + "200": { + "description": "Sync events", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "aggregate_id": { + "type": "string" + }, + "seq": { + "type": "number" + }, + "type": { + "type": "string" + }, + "data": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["id", "aggregate_id", "seq", "type", "data"] + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.history.list({\n ...\n})" + } + ] + } + }, "/find": { "get": { "operationId": "find.text", @@ -8514,6 +8794,40 @@ }, "required": ["type", "properties"] }, + "Event.workspace.restore": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "workspace.restore" + }, + "properties": { + "type": "object", + "properties": { + "workspaceID": { + "type": "string", + "pattern": "^wrk.*" + }, + "sessionID": { + "type": "string", + "pattern": "^ses.*" + }, + "total": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "step": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["workspaceID", "sessionID", "total", "step"] + } + }, + "required": ["type", "properties"] + }, "Event.workspace.status": { "type": "object", "properties": { @@ -10523,6 +10837,9 @@ { "$ref": "#/components/schemas/Event.workspace.failed" }, + { + "$ref": "#/components/schemas/Event.workspace.restore" + }, { "$ref": "#/components/schemas/Event.workspace.status" }, @@ -12780,6 +13097,9 @@ { "$ref": "#/components/schemas/Event.workspace.failed" }, + { + "$ref": "#/components/schemas/Event.workspace.restore" + }, { "$ref": "#/components/schemas/Event.workspace.status" }, -- cgit v1.2.3 From 3b75f16119b914fa6eb7dd451a0f3fb20d22d69f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 15 Apr 2026 21:29:10 +0000 Subject: chore: generate --- packages/sdk/openapi.json | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) (limited to 'packages/sdk/openapi.json') diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index ee3538d55..c59e1ab91 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8143,7 +8143,8 @@ "type": "object", "properties": { "messageID": { - "type": "string" + "type": "string", + "pattern": "^msg.*" }, "callID": { "type": "string" @@ -8155,10 +8156,12 @@ "type": "object", "properties": { "id": { - "type": "string" + "type": "string", + "pattern": "^que.*" }, "sessionID": { - "type": "string" + "type": "string", + "pattern": "^ses.*" }, "questions": { "description": "Questions to ask", @@ -8196,10 +8199,12 @@ "type": "object", "properties": { "sessionID": { - "type": "string" + "type": "string", + "pattern": "^ses.*" }, "requestID": { - "type": "string" + "type": "string", + "pattern": "^que.*" }, "answers": { "type": "array", @@ -8227,10 +8232,12 @@ "type": "object", "properties": { "sessionID": { - "type": "string" + "type": "string", + "pattern": "^ses.*" }, "requestID": { - "type": "string" + "type": "string", + "pattern": "^que.*" } }, "required": ["sessionID", "requestID"] -- cgit v1.2.3 From 64cc4623b54a45c6d399110dbe4e147ef050dc8c Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 16 Apr 2026 02:08:47 +0000 Subject: chore: generate --- packages/sdk/openapi.json | 48 ++++++++--------------------------------------- 1 file changed, 8 insertions(+), 40 deletions(-) (limited to 'packages/sdk/openapi.json') diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index c59e1ab91..f63d12490 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -12699,16 +12699,8 @@ "type": "object", "properties": { "type": { - "anyOf": [ - { - "type": "string", - "const": "oauth" - }, - { - "type": "string", - "const": "api" - } - ] + "type": "string", + "enum": ["oauth", "api"] }, "label": { "type": "string" @@ -12740,16 +12732,8 @@ "type": "string" }, "op": { - "anyOf": [ - { - "type": "string", - "const": "eq" - }, - { - "type": "string", - "const": "neq" - } - ] + "type": "string", + "enum": ["eq", "neq"] }, "value": { "type": "string" @@ -12798,16 +12782,8 @@ "type": "string" }, "op": { - "anyOf": [ - { - "type": "string", - "const": "eq" - }, - { - "type": "string", - "const": "neq" - } - ] + "type": "string", + "enum": ["eq", "neq"] }, "value": { "type": "string" @@ -12831,16 +12807,8 @@ "type": "string" }, "method": { - "anyOf": [ - { - "type": "string", - "const": "auto" - }, - { - "type": "string", - "const": "code" - } - ] + "type": "string", + "enum": ["auto", "code"] }, "instructions": { "type": "string" -- cgit v1.2.3 From 7341718f9234b0cf3a8758c87e91d2006b71bff6 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 16 Apr 2026 07:15:05 +0000 Subject: chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 36 ++++++------ packages/sdk/openapi.json | 100 ++++++++++++++++---------------- 2 files changed, 68 insertions(+), 68 deletions(-) (limited to 'packages/sdk/openapi.json') diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 24c1d53bf..795c2f264 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -40,20 +40,6 @@ export type EventServerInstanceDisposed = { } } -export type EventInstallationUpdated = { - type: "installation.updated" - properties: { - version: string - } -} - -export type EventInstallationUpdateAvailable = { - type: "installation.update-available" - properties: { - version: string - } -} - export type EventServerConnected = { type: "server.connected" properties: { @@ -98,6 +84,20 @@ export type EventLspUpdated = { } } +export type EventInstallationUpdated = { + type: "installation.updated" + properties: { + version: string + } +} + +export type EventInstallationUpdateAvailable = { + type: "installation.update-available" + properties: { + version: string + } +} + export type EventMessagePartDelta = { type: "message.part.delta" properties: { @@ -1111,14 +1111,14 @@ export type GlobalEvent = { payload: | EventProjectUpdated | EventServerInstanceDisposed - | EventInstallationUpdated - | EventInstallationUpdateAvailable | EventServerConnected | EventGlobalDisposed | EventFileEdited | EventFileWatcherUpdated | EventLspClientDiagnostics | EventLspUpdated + | EventInstallationUpdated + | EventInstallationUpdateAvailable | EventMessagePartDelta | EventPermissionAsked | EventPermissionReplied @@ -2024,14 +2024,14 @@ export type File = { export type Event = | EventProjectUpdated | EventServerInstanceDisposed - | EventInstallationUpdated - | EventInstallationUpdateAvailable | EventServerConnected | EventGlobalDisposed | EventFileEdited | EventFileWatcherUpdated | EventLspClientDiagnostics | EventLspUpdated + | EventInstallationUpdated + | EventInstallationUpdateAvailable | EventMessagePartDelta | EventPermissionAsked | EventPermissionReplied diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index f63d12490..d11f9ad73 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7560,44 +7560,6 @@ }, "required": ["type", "properties"] }, - "Event.installation.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "installation.updated" - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"] - } - }, - "required": ["type", "properties"] - }, - "Event.installation.update-available": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "installation.update-available" - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"] - } - }, - "required": ["type", "properties"] - }, "Event.server.connected": { "type": "object", "properties": { @@ -7716,6 +7678,44 @@ }, "required": ["type", "properties"] }, + "Event.installation.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "installation.updated" + }, + "properties": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"] + } + }, + "required": ["type", "properties"] + }, + "Event.installation.update-available": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "installation.update-available" + }, + "properties": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"] + } + }, + "required": ["type", "properties"] + }, "Event.message.part.delta": { "type": "object", "properties": { @@ -10736,12 +10736,6 @@ { "$ref": "#/components/schemas/Event.server.instance.disposed" }, - { - "$ref": "#/components/schemas/Event.installation.updated" - }, - { - "$ref": "#/components/schemas/Event.installation.update-available" - }, { "$ref": "#/components/schemas/Event.server.connected" }, @@ -10760,6 +10754,12 @@ { "$ref": "#/components/schemas/Event.lsp.updated" }, + { + "$ref": "#/components/schemas/Event.installation.updated" + }, + { + "$ref": "#/components/schemas/Event.installation.update-available" + }, { "$ref": "#/components/schemas/Event.message.part.delta" }, @@ -12964,12 +12964,6 @@ { "$ref": "#/components/schemas/Event.server.instance.disposed" }, - { - "$ref": "#/components/schemas/Event.installation.updated" - }, - { - "$ref": "#/components/schemas/Event.installation.update-available" - }, { "$ref": "#/components/schemas/Event.server.connected" }, @@ -12988,6 +12982,12 @@ { "$ref": "#/components/schemas/Event.lsp.updated" }, + { + "$ref": "#/components/schemas/Event.installation.updated" + }, + { + "$ref": "#/components/schemas/Event.installation.update-available" + }, { "$ref": "#/components/schemas/Event.message.part.delta" }, -- cgit v1.2.3