chore: generate

This commit is contained in:
opencode-agent[bot]
2026-04-30 15:46:04 +00:00
parent 53e9cac383
commit 8f57a2a462
5 changed files with 782 additions and 662 deletions
@@ -1,4 +1,3 @@
This is a plugin to simulate a remote environment locally. Add this to `.opencode/opencode.jsonc`: This is a plugin to simulate a remote environment locally. Add this to `.opencode/opencode.jsonc`:
```json ```json
@@ -15,6 +14,6 @@ With the plugin install, you can now run OpenCode and create a `debug` workspace
How this works: How this works:
* The workspace server needs to know the workspace id and port to run. It waits for this information to be written to a file and starts the server when the data is written. - The workspace server needs to know the workspace id and port to run. It waits for this information to be written to a file and starts the server when the data is written.
* The debug plugin writes this information in the `create` call to the workspace. So create a `debug` workspace will always kick off a new external server. - The debug plugin writes this information in the `create` call to the workspace. So create a `debug` workspace will always kick off a new external server.
* The server script watches for file changes, so whenver you create a new `debug` workspace it will restart with the new information. This means that there is only ever one working `debug` workspace at a time; when you create a new one all previous sessions will show that it can't connect because previous debug workspaces do not exist. - The server script watches for file changes, so whenver you create a new `debug` workspace it will restart with the new information. This means that there is only ever one working `debug` workspace at a time; when you create a new one all previous sessions will show that it can't connect because previous debug workspaces do not exist.
@@ -101,10 +101,13 @@ export class SyncHttpError extends Schema.TaggedErrorClass<SyncHttpError>()("Wor
body: Schema.optional(Schema.String), body: Schema.optional(Schema.String),
}) {} }) {}
export class WorkspaceNotFoundError extends Schema.TaggedErrorClass<WorkspaceNotFoundError>()("WorkspaceNotFoundError", { export class WorkspaceNotFoundError extends Schema.TaggedErrorClass<WorkspaceNotFoundError>()(
"WorkspaceNotFoundError",
{
message: Schema.String, message: Schema.String,
workspaceID: WorkspaceID, workspaceID: WorkspaceID,
}) {} },
) {}
export class SessionEventsNotFoundError extends Schema.TaggedErrorClass<SessionEventsNotFoundError>()( export class SessionEventsNotFoundError extends Schema.TaggedErrorClass<SessionEventsNotFoundError>()(
"WorkspaceSessionEventsNotFoundError", "WorkspaceSessionEventsNotFoundError",
@@ -170,7 +170,11 @@ function recordedAdaptor(input: {
return input.configure?.(info) ?? info return input.configure?.(info) ?? info
}, },
async create(info, env, from) { async create(info, env, from) {
calls.create.push({ info: structuredClone(info), env: { ...env }, from: from ? structuredClone(from) : undefined }) calls.create.push({
info: structuredClone(info),
env: { ...env },
from: from ? structuredClone(from) : undefined,
})
await input.create?.(info, env, from) await input.create?.(info, env, from)
}, },
async remove(info) { async remove(info) {
@@ -279,12 +283,18 @@ function insertProject(id: ProjectID, worktree: string) {
} }
function attachSessionToWorkspace(sessionID: SessionID, workspaceID: WorkspaceID) { function attachSessionToWorkspace(sessionID: SessionID, workspaceID: WorkspaceID) {
Database.use((db) => db.update(SessionTable).set({ workspace_id: workspaceID }).where(eq(SessionTable.id, sessionID)).run()) Database.use((db) =>
db.update(SessionTable).set({ workspace_id: workspaceID }).where(eq(SessionTable.id, sessionID)).run(),
)
} }
function sessionSequence(sessionID: SessionID) { function sessionSequence(sessionID: SessionID) {
return Database.use((db) => return Database.use((db) =>
db.select({ seq: EventSequenceTable.seq }).from(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, sessionID)).get(), db
.select({ seq: EventSequenceTable.seq })
.from(EventSequenceTable)
.where(eq(EventSequenceTable.aggregate_id, sessionID))
.get(),
)?.seq )?.seq
} }
@@ -308,7 +318,9 @@ function replaceSessionEvents(sessionID: SessionID, count: number) {
db.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, sessionID)).run() db.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, sessionID)).run()
if (count === 0) return if (count === 0) return
db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: count - 1 }).run() db.insert(EventSequenceTable)
.values({ aggregate_id: sessionID, seq: count - 1 })
.run()
db.insert(EventTable) db.insert(EventTable)
.values( .values(
Array.from({ length: count }, (_, i) => ({ Array.from({ length: count }, (_, i) => ({
@@ -522,7 +534,8 @@ describe("workspace-old CRUD", () => {
it.live("remote create connects to routed event and history endpoints", () => { it.live("remote create connects to routed event and history endpoints", () => {
const calls: FetchCall[] = [] const calls: FetchCall[] = []
return Effect.gen(function* () { return Effect.gen(function* () {
yield* HttpServer.serveEffect()(Effect.gen(function* () { yield* HttpServer.serveEffect()(
Effect.gen(function* () {
const req = yield* HttpServerRequest.HttpServerRequest const req = yield* HttpServerRequest.HttpServerRequest
const bodyText = yield* req.text const bodyText = yield* req.text
const call = { const call = {
@@ -533,12 +546,15 @@ describe("workspace-old CRUD", () => {
json: bodyText ? JSON.parse(bodyText) : undefined, json: bodyText ? JSON.parse(bodyText) : undefined,
} }
calls.push(call) calls.push(call)
if (call.url.pathname === "/base/global/event") return HttpServerResponse.fromWeb(eventStreamResponse([], false)) if (call.url.pathname === "/base/global/event")
return HttpServerResponse.fromWeb(eventStreamResponse([], false))
if (call.url.pathname === "/base/sync/history") return yield* HttpServerResponse.json([]) if (call.url.pathname === "/base/sync/history") return yield* HttpServerResponse.json([])
return HttpServerResponse.text("unexpected", { status: 500 }) return HttpServerResponse.text("unexpected", { status: 500 })
})) }),
)
const url = yield* serverUrl() const url = yield* serverUrl()
yield* provideTmpdirInstance((dir) => yield* provideTmpdirInstance(
(dir) =>
Effect.gen(function* () { Effect.gen(function* () {
const workspace = yield* WorkspaceOld.Service const workspace = yield* WorkspaceOld.Service
const type = unique("remote-create") const type = unique("remote-create")
@@ -547,10 +563,9 @@ describe("workspace-old CRUD", () => {
const info = yield* workspace.create({ type, branch: null, projectID: Instance.project.id, extra: null }) const info = yield* workspace.create({ type, branch: null, projectID: Instance.project.id, extra: null })
expect(calls.map((call) => `${call.method} ${call.url.pathname}${call.url.search}${call.url.hash}`)).toEqual([ expect(
"GET /base/global/event", calls.map((call) => `${call.method} ${call.url.pathname}${call.url.search}${call.url.hash}`),
"POST /base/sync/history", ).toEqual(["GET /base/global/event", "POST /base/sync/history"])
])
expect(calls[1].json).toEqual({}) expect(calls[1].json).toEqual({})
expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("connected") expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("connected")
expect(yield* workspace.isSyncing(info.id)).toBe(true) expect(yield* workspace.isSyncing(info.id)).toBe(true)
@@ -651,11 +666,16 @@ describe("workspace-old sync state", () => {
insertWorkspace(withoutSession) insertWorkspace(withoutSession)
registerAdaptor(Instance.project.id, withSessionType, localAdaptor(withSessionDir).adaptor) registerAdaptor(Instance.project.id, withSessionType, localAdaptor(withSessionDir).adaptor)
registerAdaptor(Instance.project.id, withoutSessionType, localAdaptor(withoutSessionDir).adaptor) registerAdaptor(Instance.project.id, withoutSessionType, localAdaptor(withoutSessionDir).adaptor)
attachSessionToWorkspace((await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id, withSession.id) attachSessionToWorkspace(
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
withSession.id,
)
WorkspaceOld.startWorkspaceSyncing(Instance.project.id) WorkspaceOld.startWorkspaceSyncing(Instance.project.id)
await eventually(() => expect(WorkspaceOld.status().find((item) => item.workspaceID === withSession.id)?.status).toBe("connected")) await eventually(() =>
expect(WorkspaceOld.status().find((item) => item.workspaceID === withSession.id)?.status).toBe("connected"),
)
expect(WorkspaceOld.status().find((item) => item.workspaceID === withoutSession.id)?.status).toBeUndefined() expect(WorkspaceOld.status().find((item) => item.workspaceID === withoutSession.id)?.status).toBeUndefined()
await WorkspaceOld.remove(withSession.id) await WorkspaceOld.remove(withSession.id)
await WorkspaceOld.remove(withoutSession.id) await WorkspaceOld.remove(withoutSession.id)
@@ -667,12 +687,21 @@ describe("workspace-old sync state", () => {
const type = unique("missing-local") const type = unique("missing-local")
const info = workspaceInfo(Instance.project.id, type) const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, localAdaptor(path.join(dir, "missing-target"), { createDir: false }).adaptor) registerAdaptor(
attachSessionToWorkspace((await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id, info.id) Instance.project.id,
type,
localAdaptor(path.join(dir, "missing-target"), { createDir: false }).adaptor,
)
attachSessionToWorkspace(
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
info.id,
)
WorkspaceOld.startWorkspaceSyncing(Instance.project.id) WorkspaceOld.startWorkspaceSyncing(Instance.project.id)
await eventually(() => expect(WorkspaceOld.status().find((item) => item.workspaceID === info.id)?.status).toBe("error")) await eventually(() =>
expect(WorkspaceOld.status().find((item) => item.workspaceID === info.id)?.status).toBe("error"),
)
expect(await WorkspaceOld.isSyncing(info.id)).toBe(false) expect(await WorkspaceOld.isSyncing(info.id)).toBe(false)
await WorkspaceOld.remove(info.id) await WorkspaceOld.remove(info.id)
}) })
@@ -688,12 +717,17 @@ describe("workspace-old sync state", () => {
await fs.mkdir(target, { recursive: true }) await fs.mkdir(target, { recursive: true })
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, localAdaptor(target).adaptor) registerAdaptor(Instance.project.id, type, localAdaptor(target).adaptor)
attachSessionToWorkspace((await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id, info.id) attachSessionToWorkspace(
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
info.id,
)
WorkspaceOld.startWorkspaceSyncing(Instance.project.id) WorkspaceOld.startWorkspaceSyncing(Instance.project.id)
WorkspaceOld.startWorkspaceSyncing(Instance.project.id) WorkspaceOld.startWorkspaceSyncing(Instance.project.id)
await eventually(() => expect(WorkspaceOld.status().find((item) => item.workspaceID === info.id)?.status).toBe("connected")) await eventually(() =>
expect(WorkspaceOld.status().find((item) => item.workspaceID === info.id)?.status).toBe("connected"),
)
expect( expect(
captured.events.filter( captured.events.filter(
(event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Status.type, (event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Status.type,
@@ -709,7 +743,8 @@ describe("workspace-old sync state", () => {
it.live("remote start emits disconnected, connecting, and connected then refuses duplicate listeners", () => { it.live("remote start emits disconnected, connecting, and connected then refuses duplicate listeners", () => {
const calls: FetchCall[] = [] const calls: FetchCall[] = []
return Effect.gen(function* () { return Effect.gen(function* () {
yield* HttpServer.serveEffect()(Effect.gen(function* () { yield* HttpServer.serveEffect()(
Effect.gen(function* () {
const req = yield* HttpServerRequest.HttpServerRequest const req = yield* HttpServerRequest.HttpServerRequest
const bodyText = yield* req.text const bodyText = yield* req.text
const call = { const call = {
@@ -723,9 +758,11 @@ describe("workspace-old sync state", () => {
if (call.url.pathname === "/sync/global/event") return HttpServerResponse.fromWeb(eventStreamResponse()) if (call.url.pathname === "/sync/global/event") return HttpServerResponse.fromWeb(eventStreamResponse())
if (call.url.pathname === "/sync/sync/history") return HttpServerResponse.fromWeb(Response.json([])) if (call.url.pathname === "/sync/sync/history") return HttpServerResponse.fromWeb(Response.json([]))
return HttpServerResponse.text("unexpected", { status: 500 }) return HttpServerResponse.text("unexpected", { status: 500 })
})) }),
)
const url = yield* serverUrl() const url = yield* serverUrl()
yield* provideTmpdirInstance(() => yield* provideTmpdirInstance(
() =>
Effect.gen(function* () { Effect.gen(function* () {
const workspace = yield* WorkspaceOld.Service const workspace = yield* WorkspaceOld.Service
const sessionSvc = yield* SessionNs.Service const sessionSvc = yield* SessionNs.Service
@@ -738,15 +775,21 @@ describe("workspace-old sync state", () => {
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id) attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
yield* workspace.startWorkspaceSyncing(Instance.project.id) yield* workspace.startWorkspaceSyncing(Instance.project.id)
yield* eventuallyEffect(Effect.gen(function* () { yield* eventuallyEffect(
expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("connected") Effect.gen(function* () {
})) expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe(
"connected",
)
}),
)
yield* workspace.startWorkspaceSyncing(Instance.project.id) yield* workspace.startWorkspaceSyncing(Instance.project.id)
yield* Effect.sleep("25 millis") yield* Effect.sleep("25 millis")
expect( expect(
captured.events captured.events
.filter((event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Status.type) .filter(
(event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Status.type,
)
.map((event) => event.payload.properties.status), .map((event) => event.payload.properties.status),
).toEqual(["disconnected", "connecting", "connected"]) ).toEqual(["disconnected", "connecting", "connected"])
expect(calls.filter((call) => call.url.pathname === "/sync/global/event")).toHaveLength(1) expect(calls.filter((call) => call.url.pathname === "/sync/global/event")).toHaveLength(1)
@@ -766,14 +809,17 @@ describe("workspace-old sync state", () => {
it.live("remote connection HTTP failures set error and clear syncing", () => it.live("remote connection HTTP failures set error and clear syncing", () =>
Effect.gen(function* () { Effect.gen(function* () {
yield* HttpServer.serveEffect()(Effect.gen(function* () { yield* HttpServer.serveEffect()(
Effect.gen(function* () {
const req = yield* HttpServerRequest.HttpServerRequest const req = yield* HttpServerRequest.HttpServerRequest
if (new URL(req.url, "http://localhost").pathname === "/failed/global/event") if (new URL(req.url, "http://localhost").pathname === "/failed/global/event")
return HttpServerResponse.text("nope", { status: 503 }) return HttpServerResponse.text("nope", { status: 503 })
return HttpServerResponse.fromWeb(Response.json([])) return HttpServerResponse.fromWeb(Response.json([]))
})) }),
)
const url = yield* serverUrl() const url = yield* serverUrl()
yield* provideTmpdirInstance(() => yield* provideTmpdirInstance(
() =>
Effect.gen(function* () { Effect.gen(function* () {
const workspace = yield* WorkspaceOld.Service const workspace = yield* WorkspaceOld.Service
const sessionSvc = yield* SessionNs.Service const sessionSvc = yield* SessionNs.Service
@@ -785,9 +831,11 @@ describe("workspace-old sync state", () => {
yield* workspace.startWorkspaceSyncing(Instance.project.id) yield* workspace.startWorkspaceSyncing(Instance.project.id)
yield* eventuallyEffect(Effect.gen(function* () { yield* eventuallyEffect(
Effect.gen(function* () {
expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("error") expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("error")
})) }),
)
expect(yield* workspace.isSyncing(info.id)).toBe(false) expect(yield* workspace.isSyncing(info.id)).toBe(false)
yield* workspace.remove(info.id) yield* workspace.remove(info.id)
}), }),
@@ -798,15 +846,20 @@ describe("workspace-old sync state", () => {
it.live("remote history HTTP failures set error", () => it.live("remote history HTTP failures set error", () =>
Effect.gen(function* () { Effect.gen(function* () {
yield* HttpServer.serveEffect()(Effect.gen(function* () { yield* HttpServer.serveEffect()(
Effect.gen(function* () {
const req = yield* HttpServerRequest.HttpServerRequest const req = yield* HttpServerRequest.HttpServerRequest
const url = new URL(req.url, "http://localhost") const url = new URL(req.url, "http://localhost")
if (url.pathname === "/history-failed/global/event") return HttpServerResponse.fromWeb(eventStreamResponse([], false)) if (url.pathname === "/history-failed/global/event")
if (url.pathname === "/history-failed/sync/history") return HttpServerResponse.text("history failed", { status: 500 }) return HttpServerResponse.fromWeb(eventStreamResponse([], false))
if (url.pathname === "/history-failed/sync/history")
return HttpServerResponse.text("history failed", { status: 500 })
return HttpServerResponse.fromWeb(Response.json([])) return HttpServerResponse.fromWeb(Response.json([]))
})) }),
)
const url = yield* serverUrl() const url = yield* serverUrl()
yield* provideTmpdirInstance(() => yield* provideTmpdirInstance(
() =>
Effect.gen(function* () { Effect.gen(function* () {
const workspace = yield* WorkspaceOld.Service const workspace = yield* WorkspaceOld.Service
const sessionSvc = yield* SessionNs.Service const sessionSvc = yield* SessionNs.Service
@@ -818,9 +871,11 @@ describe("workspace-old sync state", () => {
yield* workspace.startWorkspaceSyncing(Instance.project.id) yield* workspace.startWorkspaceSyncing(Instance.project.id)
yield* eventuallyEffect(Effect.gen(function* () { yield* eventuallyEffect(
Effect.gen(function* () {
expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("error") expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBe("error")
})) }),
)
expect(yield* workspace.isSyncing(info.id)).toBe(false) expect(yield* workspace.isSyncing(info.id)).toBe(false)
yield* workspace.remove(info.id) yield* workspace.remove(info.id)
}), }),
@@ -834,7 +889,8 @@ describe("workspace-old sync state", () => {
let historySessionID: SessionID | undefined let historySessionID: SessionID | undefined
let historyNextSeq = 0 let historyNextSeq = 0
return Effect.gen(function* () { return Effect.gen(function* () {
yield* HttpServer.serveEffect()(Effect.gen(function* () { yield* HttpServer.serveEffect()(
Effect.gen(function* () {
const req = yield* HttpServerRequest.HttpServerRequest const req = yield* HttpServerRequest.HttpServerRequest
const bodyText = yield* req.text const bodyText = yield* req.text
const url = new URL(req.url, "http://localhost") const url = new URL(req.url, "http://localhost")
@@ -854,9 +910,11 @@ describe("workspace-old sync state", () => {
) )
} }
return HttpServerResponse.text("unexpected", { status: 500 }) return HttpServerResponse.text("unexpected", { status: 500 })
})) }),
)
const url = yield* serverUrl() const url = yield* serverUrl()
yield* provideTmpdirInstance(() => yield* provideTmpdirInstance(
() =>
Effect.gen(function* () { Effect.gen(function* () {
const workspace = yield* WorkspaceOld.Service const workspace = yield* WorkspaceOld.Service
const sessionSvc = yield* SessionNs.Service const sessionSvc = yield* SessionNs.Service
@@ -873,14 +931,18 @@ describe("workspace-old sync state", () => {
yield* workspace.startWorkspaceSyncing(Instance.project.id) yield* workspace.startWorkspaceSyncing(Instance.project.id)
yield* eventuallyEffect(Effect.gen(function* () { yield* eventuallyEffect(
Effect.gen(function* () {
expect((yield* sessionSvc.get(session.id)).title).toBe("from history") expect((yield* sessionSvc.get(session.id)).title).toBe("from history")
})) }),
)
expect(historyBodies).toEqual([{ [session.id]: historyNextSeq - 1 }]) expect(historyBodies).toEqual([{ [session.id]: historyNextSeq - 1 }])
expect( expect(
captured.events.some( captured.events.some(
(event) => (event) =>
event.workspace === info.id && event.payload.type === "sync" && event.payload.syncEvent.seq === historyNextSeq, event.workspace === info.id &&
event.payload.type === "sync" &&
event.payload.syncEvent.seq === historyNextSeq,
), ),
).toBe(true) ).toBe(true)
yield* workspace.remove(info.id) yield* workspace.remove(info.id)
@@ -895,7 +957,8 @@ describe("workspace-old sync state", () => {
it.live("SSE forwards non-heartbeat events and ignores heartbeats", () => it.live("SSE forwards non-heartbeat events and ignores heartbeats", () =>
Effect.gen(function* () { Effect.gen(function* () {
yield* HttpServer.serveEffect()(Effect.gen(function* () { yield* HttpServer.serveEffect()(
Effect.gen(function* () {
const req = yield* HttpServerRequest.HttpServerRequest const req = yield* HttpServerRequest.HttpServerRequest
const url = new URL(req.url, "http://localhost") const url = new URL(req.url, "http://localhost")
if (url.pathname === "/sse-forward/global/event") if (url.pathname === "/sse-forward/global/event")
@@ -914,9 +977,11 @@ describe("workspace-old sync state", () => {
) )
if (url.pathname === "/sse-forward/sync/history") return HttpServerResponse.fromWeb(Response.json([])) if (url.pathname === "/sse-forward/sync/history") return HttpServerResponse.fromWeb(Response.json([]))
return HttpServerResponse.text("unexpected", { status: 500 }) return HttpServerResponse.text("unexpected", { status: 500 })
})) }),
)
const url = yield* serverUrl() const url = yield* serverUrl()
yield* provideTmpdirInstance(() => yield* provideTmpdirInstance(
() =>
Effect.gen(function* () { Effect.gen(function* () {
const workspace = yield* WorkspaceOld.Service const workspace = yield* WorkspaceOld.Service
const sessionSvc = yield* SessionNs.Service const sessionSvc = yield* SessionNs.Service
@@ -930,16 +995,27 @@ describe("workspace-old sync state", () => {
yield* workspace.startWorkspaceSyncing(Instance.project.id) yield* workspace.startWorkspaceSyncing(Instance.project.id)
yield* eventuallyEffect(Effect.sync(() => yield* eventuallyEffect(
expect(captured.events.some((event) => event.workspace === info.id && event.payload.type === "custom.remote")) Effect.sync(() =>
.toBe(true), expect(
)) captured.events.some(
expect(captured.events.some((event) => event.workspace === info.id && event.payload.type === "server.heartbeat")).toBe( (event) => event.workspace === info.id && event.payload.type === "custom.remote",
false, ),
).toBe(true),
),
) )
expect(
captured.events.some(
(event) => event.workspace === info.id && event.payload.type === "server.heartbeat",
),
).toBe(false)
expect( expect(
captured.events.find((event) => event.workspace === info.id && event.payload.type === "custom.remote"), captured.events.find((event) => event.workspace === info.id && event.payload.type === "custom.remote"),
).toMatchObject({ directory: "remote-dir", project: "remote-project", payload: { properties: { ok: true } } }) ).toMatchObject({
directory: "remote-dir",
project: "remote-project",
payload: { properties: { ok: true } },
})
yield* workspace.remove(info.id) yield* workspace.remove(info.id)
} finally { } finally {
captured.dispose() captured.dispose()
@@ -954,7 +1030,8 @@ describe("workspace-old sync state", () => {
let sseSessionID: SessionID | undefined let sseSessionID: SessionID | undefined
let sseNextSeq = 0 let sseNextSeq = 0
return Effect.gen(function* () { return Effect.gen(function* () {
yield* HttpServer.serveEffect()(Effect.gen(function* () { yield* HttpServer.serveEffect()(
Effect.gen(function* () {
const req = yield* HttpServerRequest.HttpServerRequest const req = yield* HttpServerRequest.HttpServerRequest
const url = new URL(req.url, "http://localhost") const url = new URL(req.url, "http://localhost")
if (url.pathname === "/sse-sync/global/event") if (url.pathname === "/sse-sync/global/event")
@@ -981,9 +1058,11 @@ describe("workspace-old sync state", () => {
) )
if (url.pathname === "/sse-sync/sync/history") return HttpServerResponse.fromWeb(Response.json([])) if (url.pathname === "/sse-sync/sync/history") return HttpServerResponse.fromWeb(Response.json([]))
return HttpServerResponse.text("unexpected", { status: 500 }) return HttpServerResponse.text("unexpected", { status: 500 })
})) }),
)
const url = yield* serverUrl() const url = yield* serverUrl()
yield* provideTmpdirInstance(() => yield* provideTmpdirInstance(
() =>
Effect.gen(function* () { Effect.gen(function* () {
const workspace = yield* WorkspaceOld.Service const workspace = yield* WorkspaceOld.Service
const sessionSvc = yield* SessionNs.Service const sessionSvc = yield* SessionNs.Service
@@ -1000,12 +1079,17 @@ describe("workspace-old sync state", () => {
yield* workspace.startWorkspaceSyncing(Instance.project.id) yield* workspace.startWorkspaceSyncing(Instance.project.id)
yield* eventuallyEffect(Effect.gen(function* () { yield* eventuallyEffect(
Effect.gen(function* () {
expect((yield* sessionSvc.get(session.id)).title).toBe("from sse") expect((yield* sessionSvc.get(session.id)).title).toBe("from sse")
})) }),
)
expect( expect(
captured.events.some( captured.events.some(
(event) => event.workspace === info.id && event.payload.type === "sync" && event.payload.syncEvent.seq === sseNextSeq, (event) =>
event.workspace === info.id &&
event.payload.type === "sync" &&
event.payload.syncEvent.seq === sseNextSeq,
), ),
).toBe(true) ).toBe(true)
yield* workspace.remove(info.id) yield* workspace.remove(info.id)
@@ -1031,8 +1115,12 @@ describe("workspace-old waitForSync", () => {
const sessionID = SessionID.descending("ses_wait_done") const sessionID = SessionID.descending("ses_wait_done")
Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 4 }).run()) Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 4 }).run())
await expect(WorkspaceOld.waitForSync(WorkspaceID.ascending("wrk_wait_done"), { [sessionID]: 4 })).resolves.toBeUndefined() await expect(
await expect(WorkspaceOld.waitForSync(WorkspaceID.ascending("wrk_wait_done_2"), { [sessionID]: 3 })).resolves.toBeUndefined() WorkspaceOld.waitForSync(WorkspaceID.ascending("wrk_wait_done"), { [sessionID]: 4 }),
).resolves.toBeUndefined()
await expect(
WorkspaceOld.waitForSync(WorkspaceID.ascending("wrk_wait_done_2"), { [sessionID]: 3 }),
).resolves.toBeUndefined()
}) })
}) })
@@ -1084,13 +1172,15 @@ describe("workspace-old waitForSync", () => {
) )
abort.abort(reason) abort.abort(reason)
await expect(waited).rejects.toMatchObject({ _tag: "WorkspaceSyncAbortedError", message: reason.message, cause: reason }) await expect(waited).rejects.toMatchObject({
_tag: "WorkspaceSyncAbortedError",
message: reason.message,
cause: reason,
})
}) })
}) })
test( test("times out with the requested fence in the error message", async () => {
"times out with the requested fence in the error message",
async () => {
await withInstance(async () => { await withInstance(async () => {
const sessionID = SessionID.descending("ses_wait_timeout") const sessionID = SessionID.descending("ses_wait_timeout")
@@ -1098,9 +1188,7 @@ describe("workspace-old waitForSync", () => {
WorkspaceOld.waitForSync(WorkspaceID.ascending("wrk_wait_timeout"), { [sessionID]: 1 }), WorkspaceOld.waitForSync(WorkspaceID.ascending("wrk_wait_timeout"), { [sessionID]: 1 }),
).rejects.toThrow(`Timed out waiting for sync fence: {"${sessionID}":1}`) ).rejects.toThrow(`Timed out waiting for sync fence: {"${sessionID}":1}`)
}) })
}, }, 7000)
7000,
)
}) })
describe("workspace-old sessionRestore", () => { describe("workspace-old sessionRestore", () => {
@@ -1132,7 +1220,8 @@ describe("workspace-old sessionRestore", () => {
it.live("posts remote replay batches of 10, emits progress, and includes the workspace update event", () => { it.live("posts remote replay batches of 10, emits progress, and includes the workspace update event", () => {
const replay: FetchCall[] = [] const replay: FetchCall[] = []
return Effect.gen(function* () { return Effect.gen(function* () {
yield* HttpServer.serveEffect()(Effect.gen(function* () { yield* HttpServer.serveEffect()(
Effect.gen(function* () {
const req = yield* HttpServerRequest.HttpServerRequest const req = yield* HttpServerRequest.HttpServerRequest
const bodyText = yield* req.text const bodyText = yield* req.text
const call = { const call = {
@@ -1147,9 +1236,11 @@ describe("workspace-old sessionRestore", () => {
return HttpServerResponse.fromWeb(Response.json({ ok: true })) return HttpServerResponse.fromWeb(Response.json({ ok: true }))
} }
return HttpServerResponse.text("unexpected", { status: 500 }) return HttpServerResponse.text("unexpected", { status: 500 })
})) }),
)
const url = yield* serverUrl() const url = yield* serverUrl()
yield* provideTmpdirInstance((dir) => yield* provideTmpdirInstance(
(dir) =>
Effect.gen(function* () { Effect.gen(function* () {
const workspace = yield* WorkspaceOld.Service const workspace = yield* WorkspaceOld.Service
const sessionSvc = yield* SessionNs.Service const sessionSvc = yield* SessionNs.Service
@@ -1183,9 +1274,13 @@ describe("workspace-old sessionRestore", () => {
expect(replay.map((call) => (call.json as { events: unknown[] }).events.length)).toEqual([10, 10, 5]) expect(replay.map((call) => (call.json as { events: unknown[] }).events.length)).toEqual([10, 10, 5])
expect(replay.map((call) => (call.json as { directory: string }).directory)).toEqual([dir, dir, dir]) expect(replay.map((call) => (call.json as { directory: string }).directory)).toEqual([dir, dir, dir])
expect( expect(
replay.flatMap((call) => (call.json as { events: Array<{ seq: number }> }).events.map((event) => event.seq)), replay.flatMap((call) =>
(call.json as { events: Array<{ seq: number }> }).events.map((event) => event.seq),
),
).toEqual(Array.from({ length: 25 }, (_, i) => i)) ).toEqual(Array.from({ length: 25 }, (_, i) => i))
expect((replay[2].json as { events: Array<{ seq: number; type: string; data: unknown }> }).events.at(-1)).toMatchObject({ expect(
(replay[2].json as { events: Array<{ seq: number; type: string; data: unknown }> }).events.at(-1),
).toMatchObject({
seq: 24, seq: 24,
type: sessionUpdatedType(), type: sessionUpdatedType(),
data: { sessionID: session.id, info: { workspaceID: info.id } }, data: { sessionID: session.id, info: { workspaceID: info.id } },
@@ -1193,7 +1288,9 @@ describe("workspace-old sessionRestore", () => {
expect((yield* sessionSvc.get(session.id)).workspaceID).toBe(info.id) expect((yield* sessionSvc.get(session.id)).workspaceID).toBe(info.id)
expect( expect(
captured.events captured.events
.filter((event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Restore.type) .filter(
(event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Restore.type,
)
.map((event) => event.payload.properties.step), .map((event) => event.payload.properties.step),
).toEqual([0, 1, 2, 3]) ).toEqual([0, 1, 2, 3])
yield* workspace.remove(info.id) yield* workspace.remove(info.id)
@@ -1209,7 +1306,8 @@ describe("workspace-old sessionRestore", () => {
it.live("remote restore sends an empty directory string when the workspace directory is null", () => { it.live("remote restore sends an empty directory string when the workspace directory is null", () => {
const replay: FetchCall[] = [] const replay: FetchCall[] = []
return Effect.gen(function* () { return Effect.gen(function* () {
yield* HttpServer.serveEffect()(Effect.gen(function* () { yield* HttpServer.serveEffect()(
Effect.gen(function* () {
const req = yield* HttpServerRequest.HttpServerRequest const req = yield* HttpServerRequest.HttpServerRequest
const bodyText = yield* req.text const bodyText = yield* req.text
replay.push({ replay.push({
@@ -1220,9 +1318,11 @@ describe("workspace-old sessionRestore", () => {
json: bodyText ? JSON.parse(bodyText) : undefined, json: bodyText ? JSON.parse(bodyText) : undefined,
}) })
return HttpServerResponse.fromWeb(Response.json({ ok: true })) return HttpServerResponse.fromWeb(Response.json({ ok: true }))
})) }),
)
const url = yield* serverUrl() const url = yield* serverUrl()
yield* provideTmpdirInstance(() => yield* provideTmpdirInstance(
() =>
Effect.gen(function* () { Effect.gen(function* () {
const workspace = yield* WorkspaceOld.Service const workspace = yield* WorkspaceOld.Service
const sessionSvc = yield* SessionNs.Service const sessionSvc = yield* SessionNs.Service
@@ -1233,7 +1333,9 @@ describe("workspace-old sessionRestore", () => {
const session = yield* sessionSvc.create({ title: "null dir" }) const session = yield* sessionSvc.create({ title: "null dir" })
replaceSessionEvents(session.id, 0) replaceSessionEvents(session.id, 0)
expect(yield* workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({ total: 1 }) expect(yield* workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({
total: 1,
})
expect((replay[0].json as { directory: string }).directory).toBe("") expect((replay[0].json as { directory: string }).directory).toBe("")
expect((replay[0].json as { events: unknown[] }).events).toHaveLength(1) expect((replay[0].json as { events: unknown[] }).events).toHaveLength(1)
yield* workspace.remove(info.id) yield* workspace.remove(info.id)
@@ -1246,7 +1348,8 @@ describe("workspace-old sessionRestore", () => {
it.live("remote restore failures include status and body and do not emit completed batch progress", () => { it.live("remote restore failures include status and body and do not emit completed batch progress", () => {
const replay: FetchCall[] = [] const replay: FetchCall[] = []
return Effect.gen(function* () { return Effect.gen(function* () {
yield* HttpServer.serveEffect()(Effect.gen(function* () { yield* HttpServer.serveEffect()(
Effect.gen(function* () {
const req = yield* HttpServerRequest.HttpServerRequest const req = yield* HttpServerRequest.HttpServerRequest
const bodyText = yield* req.text const bodyText = yield* req.text
replay.push({ replay.push({
@@ -1257,9 +1360,11 @@ describe("workspace-old sessionRestore", () => {
json: bodyText ? JSON.parse(bodyText) : undefined, json: bodyText ? JSON.parse(bodyText) : undefined,
}) })
return HttpServerResponse.text("replay failed", { status: 503 }) return HttpServerResponse.text("replay failed", { status: 503 })
})) }),
)
const url = yield* serverUrl() const url = yield* serverUrl()
yield* provideTmpdirInstance((dir) => yield* provideTmpdirInstance(
(dir) =>
Effect.gen(function* () { Effect.gen(function* () {
const workspace = yield* WorkspaceOld.Service const workspace = yield* WorkspaceOld.Service
const sessionSvc = yield* SessionNs.Service const sessionSvc = yield* SessionNs.Service
@@ -1272,7 +1377,9 @@ describe("workspace-old sessionRestore", () => {
const session = yield* sessionSvc.create({ title: "restore fail" }) const session = yield* sessionSvc.create({ title: "restore fail" })
replaceSessionEvents(session.id, 11) replaceSessionEvents(session.id, 11)
const error = yield* Effect.flip(workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id })) const error = yield* Effect.flip(
workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id }),
)
expect((error as Error).message).toContain( expect((error as Error).message).toContain(
`Failed to replay session ${session.id} into workspace ${info.id}: HTTP 503 replay failed`, `Failed to replay session ${session.id} into workspace ${info.id}: HTTP 503 replay failed`,
) )
@@ -1280,7 +1387,9 @@ describe("workspace-old sessionRestore", () => {
expect(replay).toHaveLength(1) expect(replay).toHaveLength(1)
expect( expect(
captured.events captured.events
.filter((event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Restore.type) .filter(
(event) => event.workspace === info.id && event.payload.type === WorkspaceOld.Event.Restore.type,
)
.map((event) => event.payload.properties.step), .map((event) => event.payload.properties.step),
).toEqual([0]) ).toEqual([0])
yield* workspace.remove(info.id) yield* workspace.remove(info.id)
@@ -1310,7 +1419,9 @@ describe("workspace-old sessionRestore", () => {
const info = workspaceInfo(Instance.project.id, type, { directory: dir }) const info = workspaceInfo(Instance.project.id, type, { directory: dir })
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, localAdaptor(dir).adaptor) registerAdaptor(Instance.project.id, type, localAdaptor(dir).adaptor)
const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({ title: "restore local" }))) const session = await AppRuntime.runPromise(
SessionNs.Service.use((svc) => svc.create({ title: "restore local" })),
)
replaceSessionEvents(session.id, 20) replaceSessionEvents(session.id, 20)
expect(await WorkspaceOld.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({ total: 3 }) expect(await WorkspaceOld.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({ total: 3 })
@@ -1318,7 +1429,9 @@ describe("workspace-old sessionRestore", () => {
expect(fetchCallCount).toBe(0) expect(fetchCallCount).toBe(0)
expect(replayAll).toHaveBeenCalledTimes(3) expect(replayAll).toHaveBeenCalledTimes(3)
expect(replayAll.mock.calls.map((call) => call[0].length)).toEqual([10, 10, 1]) expect(replayAll.mock.calls.map((call) => call[0].length)).toEqual([10, 10, 1])
expect((await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.get(session.id)))).workspaceID).toBe(info.id) expect((await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.get(session.id)))).workspaceID).toBe(
info.id,
)
expect(eventRows(session.id).map((row) => row.seq)).toEqual(Array.from({ length: 21 }, (_, i) => i)) expect(eventRows(session.id).map((row) => row.seq)).toEqual(Array.from({ length: 21 }, (_, i) => i))
expect( expect(
captured.events captured.events
@@ -1335,7 +1448,8 @@ describe("workspace-old sessionRestore", () => {
it.live("session restore includes real message and part events in sequence order", () => { it.live("session restore includes real message and part events in sequence order", () => {
const replay: FetchCall[] = [] const replay: FetchCall[] = []
return Effect.gen(function* () { return Effect.gen(function* () {
yield* HttpServer.serveEffect()(Effect.gen(function* () { yield* HttpServer.serveEffect()(
Effect.gen(function* () {
const req = yield* HttpServerRequest.HttpServerRequest const req = yield* HttpServerRequest.HttpServerRequest
const bodyText = yield* req.text const bodyText = yield* req.text
replay.push({ replay.push({
@@ -1346,9 +1460,11 @@ describe("workspace-old sessionRestore", () => {
json: bodyText ? JSON.parse(bodyText) : undefined, json: bodyText ? JSON.parse(bodyText) : undefined,
}) })
return HttpServerResponse.fromWeb(Response.json({ ok: true })) return HttpServerResponse.fromWeb(Response.json({ ok: true }))
})) }),
)
const url = yield* serverUrl() const url = yield* serverUrl()
yield* provideTmpdirInstance((dir) => yield* provideTmpdirInstance(
(dir) =>
Effect.gen(function* () { Effect.gen(function* () {
const workspace = yield* WorkspaceOld.Service const workspace = yield* WorkspaceOld.Service
const sessionSvc = yield* SessionNs.Service const sessionSvc = yield* SessionNs.Service
@@ -1376,7 +1492,9 @@ describe("workspace-old sessionRestore", () => {
} }
const before = eventRows(session.id) const before = eventRows(session.id)
expect(yield* workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({ total: 1 }) expect(yield* workspace.sessionRestore({ workspaceID: info.id, sessionID: session.id })).toEqual({
total: 1,
})
const posted = (replay[0].json as { events: Array<{ seq: number; type: string }> }).events const posted = (replay[0].json as { events: Array<{ seq: number; type: string }> }).events
expect(posted.map((event) => event.seq)).toEqual([...before.map((row) => row.seq), before.at(-1)!.seq + 1]) expect(posted.map((event) => event.seq)).toEqual([...before.map((row) => row.seq), before.at(-1)!.seq + 1])
+40 -40
View File
@@ -452,6 +452,38 @@ export type EventVcsBranchUpdated = {
} }
} }
export type EventWorkspaceReady = {
type: "workspace.ready"
properties: {
name: string
}
}
export type EventWorkspaceFailed = {
type: "workspace.failed"
properties: {
message: string
}
}
export type EventWorkspaceRestore = {
type: "workspace.restore"
properties: {
workspaceID: string
sessionID: string
total: number
step: number
}
}
export type EventWorkspaceStatus = {
type: "workspace.status"
properties: {
workspaceID: string
status: "connected" | "connecting" | "disconnected" | "error"
}
}
export type EventWorktreeReady = { export type EventWorktreeReady = {
type: "worktree.ready" type: "worktree.ready"
properties: { properties: {
@@ -506,38 +538,6 @@ export type EventPtyDeleted = {
} }
} }
export type EventWorkspaceReady = {
type: "workspace.ready"
properties: {
name: string
}
}
export type EventWorkspaceFailed = {
type: "workspace.failed"
properties: {
message: string
}
}
export type EventWorkspaceRestore = {
type: "workspace.restore"
properties: {
workspaceID: string
sessionID: string
total: number
step: number
}
}
export type EventWorkspaceStatus = {
type: "workspace.status"
properties: {
workspaceID: string
status: "connected" | "connecting" | "disconnected" | "error"
}
}
export type OutputFormatText = { export type OutputFormatText = {
type: "text" type: "text"
} }
@@ -1141,16 +1141,16 @@ export type GlobalEvent = {
| EventMcpBrowserOpenFailed | EventMcpBrowserOpenFailed
| EventCommandExecuted | EventCommandExecuted
| EventVcsBranchUpdated | EventVcsBranchUpdated
| EventWorkspaceReady
| EventWorkspaceFailed
| EventWorkspaceRestore
| EventWorkspaceStatus
| EventWorktreeReady | EventWorktreeReady
| EventWorktreeFailed | EventWorktreeFailed
| EventPtyCreated | EventPtyCreated
| EventPtyUpdated | EventPtyUpdated
| EventPtyExited | EventPtyExited
| EventPtyDeleted | EventPtyDeleted
| EventWorkspaceReady
| EventWorkspaceFailed
| EventWorkspaceRestore
| EventWorkspaceStatus
| EventMessageUpdated | EventMessageUpdated
| EventMessageRemoved | EventMessageRemoved
| EventMessagePartUpdated | EventMessagePartUpdated
@@ -2084,16 +2084,16 @@ export type Event =
| EventMcpBrowserOpenFailed | EventMcpBrowserOpenFailed
| EventCommandExecuted | EventCommandExecuted
| EventVcsBranchUpdated | EventVcsBranchUpdated
| EventWorkspaceReady
| EventWorkspaceFailed
| EventWorkspaceRestore
| EventWorkspaceStatus
| EventWorktreeReady | EventWorktreeReady
| EventWorktreeFailed | EventWorktreeFailed
| EventPtyCreated | EventPtyCreated
| EventPtyUpdated | EventPtyUpdated
| EventPtyExited | EventPtyExited
| EventPtyDeleted | EventPtyDeleted
| EventWorkspaceReady
| EventWorkspaceFailed
| EventWorkspaceRestore
| EventWorkspaceStatus
| EventMessageUpdated | EventMessageUpdated
| EventMessageRemoved | EventMessageRemoved
| EventMessagePartUpdated | EventMessagePartUpdated
+120 -120
View File
@@ -8743,6 +8743,102 @@
}, },
"required": ["type", "properties"] "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.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": {
"type": {
"type": "string",
"const": "workspace.status"
},
"properties": {
"type": "object",
"properties": {
"workspaceID": {
"type": "string",
"pattern": "^wrk.*"
},
"status": {
"type": "string",
"enum": ["connected", "connecting", "disconnected", "error"]
}
},
"required": ["workspaceID", "status"]
}
},
"required": ["type", "properties"]
},
"Event.worktree.ready": { "Event.worktree.ready": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -8901,102 +8997,6 @@
}, },
"required": ["type", "properties"] "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.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": {
"type": {
"type": "string",
"const": "workspace.status"
},
"properties": {
"type": "object",
"properties": {
"workspaceID": {
"type": "string",
"pattern": "^wrk.*"
},
"status": {
"type": "string",
"enum": ["connected", "connecting", "disconnected", "error"]
}
},
"required": ["workspaceID", "status"]
}
},
"required": ["type", "properties"]
},
"OutputFormatText": { "OutputFormatText": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -11047,6 +11047,18 @@
{ {
"$ref": "#/components/schemas/Event.vcs.branch.updated" "$ref": "#/components/schemas/Event.vcs.branch.updated"
}, },
{
"$ref": "#/components/schemas/Event.workspace.ready"
},
{
"$ref": "#/components/schemas/Event.workspace.failed"
},
{
"$ref": "#/components/schemas/Event.workspace.restore"
},
{
"$ref": "#/components/schemas/Event.workspace.status"
},
{ {
"$ref": "#/components/schemas/Event.worktree.ready" "$ref": "#/components/schemas/Event.worktree.ready"
}, },
@@ -11065,18 +11077,6 @@
{ {
"$ref": "#/components/schemas/Event.pty.deleted" "$ref": "#/components/schemas/Event.pty.deleted"
}, },
{
"$ref": "#/components/schemas/Event.workspace.ready"
},
{
"$ref": "#/components/schemas/Event.workspace.failed"
},
{
"$ref": "#/components/schemas/Event.workspace.restore"
},
{
"$ref": "#/components/schemas/Event.workspace.status"
},
{ {
"$ref": "#/components/schemas/Event.message.updated" "$ref": "#/components/schemas/Event.message.updated"
}, },
@@ -13340,6 +13340,18 @@
{ {
"$ref": "#/components/schemas/Event.vcs.branch.updated" "$ref": "#/components/schemas/Event.vcs.branch.updated"
}, },
{
"$ref": "#/components/schemas/Event.workspace.ready"
},
{
"$ref": "#/components/schemas/Event.workspace.failed"
},
{
"$ref": "#/components/schemas/Event.workspace.restore"
},
{
"$ref": "#/components/schemas/Event.workspace.status"
},
{ {
"$ref": "#/components/schemas/Event.worktree.ready" "$ref": "#/components/schemas/Event.worktree.ready"
}, },
@@ -13358,18 +13370,6 @@
{ {
"$ref": "#/components/schemas/Event.pty.deleted" "$ref": "#/components/schemas/Event.pty.deleted"
}, },
{
"$ref": "#/components/schemas/Event.workspace.ready"
},
{
"$ref": "#/components/schemas/Event.workspace.failed"
},
{
"$ref": "#/components/schemas/Event.workspace.restore"
},
{
"$ref": "#/components/schemas/Event.workspace.status"
},
{ {
"$ref": "#/components/schemas/Event.message.updated" "$ref": "#/components/schemas/Event.message.updated"
}, },