diff --git a/packages/server/src/api-types.ts b/packages/server/src/api-types.ts index ca9b1a0a..8104b040 100644 --- a/packages/server/src/api-types.ts +++ b/packages/server/src/api-types.ts @@ -6,6 +6,7 @@ import type { Preferences, RecentFolder, } from "./config/schema" +import type { OpenCodeEvent } from "@opencode-ai/client" /** * Canonical HTTP/SSE contract for the CLI server. @@ -278,11 +279,7 @@ export interface InstanceData { export type InstanceStreamStatus = "connecting" | "connected" | "error" | "disconnected" -export interface InstanceStreamEvent { - type: string - properties?: Record - [key: string]: unknown -} +export type InstanceStreamEvent = OpenCodeEvent export type SideCarKind = "port" diff --git a/packages/server/src/permissions/auto-accept-manager.test.ts b/packages/server/src/permissions/auto-accept-manager.test.ts index a045d2cf..e7eb7fe7 100644 --- a/packages/server/src/permissions/auto-accept-manager.test.ts +++ b/packages/server/src/permissions/auto-accept-manager.test.ts @@ -26,10 +26,15 @@ function publishInstanceEvent(bus: EventBus, instanceId: string, event: Record } | undefined + const data = event.data ?? (wrapped?.info + ? { ...wrapped.info, sessionID: wrapped.info.sessionID ?? wrapped.info.id } + : properties) + bus.publish({ type: "instance.event", instanceId, event: { ...nativeEvent, type, data } as InstanceStreamEvent }) } -/** Publish session creation using the compatibility shape produced by InstanceEventBridge. */ +/** Publish session lifecycle events using the native V2 data envelope. */ function publishSession( bus: EventBus, instanceId: string, @@ -38,7 +43,7 @@ function publishSession( ) { publishInstanceEvent(bus, instanceId, { type: eventType === "session.updated" ? "session.created" : eventType, - properties: { info: { ...info } }, + data: { ...info, sessionID: info.sessionID ?? info.id }, }) } diff --git a/packages/server/src/permissions/auto-accept-manager.ts b/packages/server/src/permissions/auto-accept-manager.ts index c1f566f4..86db4f5d 100644 --- a/packages/server/src/permissions/auto-accept-manager.ts +++ b/packages/server/src/permissions/auto-accept-manager.ts @@ -258,16 +258,16 @@ export class AutoAcceptManager { if (!event || typeof event.type !== "string") return if (SESSION_UPSERT_TYPES.has(event.type)) { - this.ingestSession(instanceId, event.properties) + this.ingestSession(instanceId, event.data) return } if (event.type === "session.forked") { - this.ingestSessionForked(instanceId, event.properties) + this.ingestSessionForked(instanceId, event.data) return } if (SESSION_REMOVE_TYPES.has(event.type)) { - const info = (event.properties as { info?: SessionProperties } | undefined)?.info - const id = readString(info?.id) ?? readString(event.properties?.id) + const data = event.data as SessionProperties | undefined + const id = readString(data?.sessionID) ?? readString(data?.id) if (id) { this.store.removeSession(instanceId, id) this.removePendingForSession(instanceId, id) @@ -275,29 +275,24 @@ export class AutoAcceptManager { return } if (PERMISSION_REPLIED_TYPES.has(event.type)) { - this.handlePermissionReplied(instanceId, event.properties) + this.handlePermissionReplied(instanceId, event.data) return } if (PERMISSION_ASK_TYPES.has(event.type)) { - this.handlePermissionRequest(instanceId, event.properties) + this.handlePermissionRequest(instanceId, event.data) } } - private ingestSession(instanceId: string, properties: unknown): void { - // OpenCode wraps session records under `properties.info` for - // session.created/updated/deleted (see SDK EventSessionUpdated). Accept a - // flat fallback only for defensive compatibility. - const info = (properties as { info?: SessionProperties } | SessionProperties | undefined) - const session = (info && typeof info === "object" && "info" in info ? info.info : info) as - | SessionProperties - | undefined - if (!session || typeof session.id !== "string") return + private ingestSession(instanceId: string, data: unknown): void { + const session = data as SessionProperties | undefined + const sessionId = readString(session?.sessionID) ?? readString(session?.id) + if (!session || !sessionId) return const parentId = session.parentID ?? session.parentId ?? null const enabledBefore = this.store.enabledRoots(instanceId) - this.store.upsertSession(instanceId, { id: session.id, parentId, fork: session.fork }) + this.store.upsertSession(instanceId, { id: sessionId, parentId, fork: session.fork }) if (typeof session.workspaceID === "string" && session.workspaceID) { const workspaces = this.sessionWorkspaces.get(instanceId) ?? new Map() - workspaces.set(session.id, session.workspaceID) + workspaces.set(sessionId, session.workspaceID) this.sessionWorkspaces.set(instanceId, workspaces) } this.persistRootMigration(instanceId, enabledBefore, this.store.enabledRoots(instanceId)) @@ -305,7 +300,7 @@ export class AutoAcceptManager { // Re-drain pending permissions whose family root may have migrated into // an enabled family — mirrors the old UI's drainAutoAcceptPermissions- // ForInstance trigger from the previous UI implementation (#497). - this.drainPending(instanceId, session.id) + this.drainPending(instanceId, sessionId) } private ingestSessionForked(instanceId: string, properties: unknown): void { @@ -484,11 +479,12 @@ export class AutoAcceptManager { interface InstanceStreamPayload { type?: string - properties?: Record + data?: unknown } interface SessionProperties { id?: string + sessionID?: string parentID?: string | null parentId?: string | null fork?: unknown diff --git a/packages/server/src/workspaces/instance-events.test.ts b/packages/server/src/workspaces/instance-events.test.ts index 12a5d5f8..05a3b8ac 100644 --- a/packages/server/src/workspaces/instance-events.test.ts +++ b/packages/server/src/workspaces/instance-events.test.ts @@ -176,18 +176,18 @@ describe("InstanceEventBridge", () => { assert.equal(received[0].instanceId, "a") assert.deepEqual(received[0].event.location, { directory: "/repo-a" }) assert.deepEqual(received[0].event.data, { id: "p1" }) - assert.deepEqual(received[0].event.properties, { id: "p1" }) + assert.equal(received[0].event.properties, undefined) assert.equal(received[1].event.data.sessionID, "session-1") - assert.equal(received[1].event.properties.info.id, "session-1") + assert.equal(received[1].event.properties, undefined) assert.equal(received[2].instanceId, "a") - assert.deepEqual(received[2].event.properties, { + assert.deepEqual(received[2].event.data, { sessionID: "session-2", assistantMessageID: "message-1", ordinal: 0, delta: "hello", }) assert.equal(received[3].instanceId, "a") - assert.equal(received[3].event.properties.delta, " again") + assert.equal(received[3].event.data.delta, " again") assert.equal(ownerLookups.get("/repo-a/.worktrees/feature"), 2) } finally { bridge.shutdown() @@ -294,7 +294,7 @@ describe("InstanceEventBridge", () => { await waitFor(() => received.length === 2) assert.equal(sessionGets(), 1) assert.deepEqual(received.map((event) => event.instanceId), ["a", "b"]) - assert.deepEqual(received.map((event) => event.event.properties.id), ["deleted", "deleted"]) + assert.deepEqual(received.map((event) => event.event.data.sessionID), ["deleted", "deleted"]) } finally { bridge.shutdown() } @@ -337,7 +337,7 @@ describe("InstanceEventBridge", () => { await waitFor(() => received.length === 1) assert.equal(sessionGets(), 1) assert.equal(received[0].instanceId, "b") - assert.equal(received[0].event.properties.form.sessionID, "owned") + assert.equal(received[0].event.data.form.sessionID, "owned") } finally { bridge.shutdown() } diff --git a/packages/server/src/workspaces/instance-events.ts b/packages/server/src/workspaces/instance-events.ts index de49d031..bd4abd5b 100644 --- a/packages/server/src/workspaces/instance-events.ts +++ b/packages/server/src/workspaces/instance-events.ts @@ -2,7 +2,7 @@ import type { OpenCodeEvent } from "@opencode-ai/client" import { EventBus } from "../events/bus" import { Logger } from "../logger" import { WorkspaceManager } from "./manager" -import { InstanceStreamEvent, InstanceStreamStatus } from "../api-types" +import { InstanceStreamStatus } from "../api-types" const RECONNECT_DELAY_MS = 1000 const DIRECTORY_OWNER_CACHE_MS = 2000 @@ -128,13 +128,8 @@ export class InstanceEventBridge { return } - // The server's auto-accept boundary still reads the legacy property name. - const compatibleEvent: InstanceStreamEvent = { - ...event, - properties: this.compatibilityProperties(event), - } for (const instanceId of instanceIds) { - this.options.eventBus.publish({ type: "instance.event", instanceId, event: compatibleEvent }) + this.options.eventBus.publish({ type: "instance.event", instanceId, event }) } if (event.type === "session.deleted" && sessionId) this.sessionDirectories.delete(sessionId) if (event.type === "pty.deleted" && ptyId) this.ptyDirectories.delete(ptyId) @@ -160,12 +155,8 @@ export class InstanceEventBridge { } private broadcastEvent(event: OpenCodeEvent): void { - const compatibleEvent: InstanceStreamEvent = { - ...event, - properties: this.compatibilityProperties(event), - } for (const workspace of this.options.workspaceManager.list()) { - this.options.eventBus.publish({ type: "instance.event", instanceId: workspace.id, event: compatibleEvent }) + this.options.eventBus.publish({ type: "instance.event", instanceId: workspace.id, event }) } } @@ -209,16 +200,6 @@ export class InstanceEventBridge { this.ptyDirectories.clear() } - private compatibilityProperties(event: OpenCodeEvent): Record { - if (event.type === "session.created") { - return { info: { ...event.data, id: event.data.sessionID } } - } - if (event.type === "session.deleted") { - return { id: event.data.sessionID } - } - return event.data as Record - } - private updateStatus(status: InstanceStreamStatus, reason?: string) { this.status = status for (const workspace of this.options.workspaceManager.list()) { diff --git a/packages/ui/src/lib/sse-manager.ts b/packages/ui/src/lib/sse-manager.ts index b28e4c86..f47af465 100644 --- a/packages/ui/src/lib/sse-manager.ts +++ b/packages/ui/src/lib/sse-manager.ts @@ -53,7 +53,6 @@ export interface WorktreeReadyEvent { export interface EventSessionDeleted { type: "session.deleted" data?: { sessionID?: string } - properties?: { info?: { id?: string }; id?: string; sessionID?: string } } type SSEEvent = diff --git a/packages/ui/src/stores/instance-runtime-authority.test.ts b/packages/ui/src/stores/instance-runtime-authority.test.ts index 98a330c9..b9f0d91a 100644 --- a/packages/ui/src/stores/instance-runtime-authority.test.ts +++ b/packages/ui/src/stores/instance-runtime-authority.test.ts @@ -79,7 +79,7 @@ describe("instance runtime authority", () => { for (const test of [ { label: "direct definitive session removal", remove: removeSessionRuntimeState }, { label: "session.deleted event", remove: (id: string, sessionId: string) => handleSessionDeleted(id, - { type: "session.deleted", properties: { info: { id: sessionId } } }) }, + { type: "session.deleted", data: { sessionID: sessionId } }) }, ]) it(`removes attachment authority on ${test.label}`, () => { const id = `authority-${test.label}`, sessionId = "deleted-session" addAttachment(id, sessionId, createTextAttachment("pasted", "pasted #1", "paste.txt")) diff --git a/packages/ui/src/stores/ptys.ts b/packages/ui/src/stores/ptys.ts index f27fdd31..a8d633dc 100644 --- a/packages/ui/src/stores/ptys.ts +++ b/packages/ui/src/stores/ptys.ts @@ -1,12 +1,13 @@ import { serverEvents } from "../lib/server-events" import { getRootClient } from "./opencode-client" -import { createPtyApi, createPtyStore } from "./pty-store" +import { createPtyApi, createPtyStore, type PtyRefreshEvent } from "./pty-store" const ptyStore = createPtyStore((instanceId) => createPtyApi(getRootClient(instanceId))) serverEvents.on("instance.event", (event) => { if (event.type !== "instance.event") return - void ptyStore.refreshForEvent(event.instanceId, event.event) + if (!event.event.type.startsWith("pty.")) return + void ptyStore.refreshForEvent(event.instanceId, event.event as PtyRefreshEvent) }) serverEvents.on("instance.eventStatus", (event) => { diff --git a/packages/ui/src/stores/session-events.ts b/packages/ui/src/stores/session-events.ts index f34f32ea..48570ebf 100644 --- a/packages/ui/src/stores/session-events.ts +++ b/packages/ui/src/stores/session-events.ts @@ -572,7 +572,7 @@ function handleSessionUpdate( } function handleSessionDeleted(instanceId: string, event: EventSessionDeleted): void { - const sessionId = event.data?.sessionID ?? event.properties?.info?.id ?? event.properties?.sessionID ?? event.properties?.id + const sessionId = event.data?.sessionID if (!sessionId) return log.info(`[SSE] Session deleted: ${sessionId}`)