diff --git a/bun.lock b/bun.lock index 2bdc2813c4..ab6d6929ae 100644 --- a/bun.lock +++ b/bun.lock @@ -1051,6 +1051,7 @@ "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch", diff --git a/package.json b/package.json index 49507128d6..b9622583a7 100644 --- a/package.json +++ b/package.json @@ -153,6 +153,7 @@ "@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", - "@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch" + "@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch", + "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch" } } diff --git a/packages/opencode/test/server/httpapi-public-openapi.test.ts b/packages/opencode/test/server/httpapi-public-openapi.test.ts index a8f6f8d1c8..310ebae595 100644 --- a/packages/opencode/test/server/httpapi-public-openapi.test.ts +++ b/packages/opencode/test/server/httpapi-public-openapi.test.ts @@ -10,6 +10,8 @@ type OpenApiSchema = { readonly enum?: readonly unknown[] readonly properties?: Record readonly required?: readonly string[] + readonly contentSchema?: OpenApiSchema + readonly contentMediaType?: string } type OpenApiResponse = { readonly description?: string @@ -99,6 +101,21 @@ describe("PublicApi OpenAPI v2 errors", () => { }) }) + test("names the v2 event union without the SSE string wrapper collision", () => { + const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec + + expect(spec.components.schemas.V2Event1).toBeUndefined() + expect(spec.components.schemas.V2Event?.anyOf?.length).toBeGreaterThan(0) + expect(spec.components.schemas.V2EventStream).toMatchObject({ + type: "string", + contentMediaType: "application/json", + contentSchema: { $ref: "#/components/schemas/V2Event" }, + }) + expect(spec.paths["/api/event"]?.get?.responses?.["200"]?.content?.["text/event-stream"]?.schema).toEqual({ + $ref: "#/components/schemas/V2Event", + }) + }) + test("preserves /api auth responses", () => { const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 8159d14591..5e067f3afb 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2766,7 +2766,7 @@ export type SessionHistory = { hasMore: boolean } -export type SessionDurableEvent1 = string +export type SessionDurableEventStream = string export type SessionMessagesResponse = { data: Array @@ -2847,7 +2847,7 @@ export type QuestionRejected2 = { } } -export type V2Event1 = +export type V2Event = | ModelsDevRefreshed | IntegrationUpdated | IntegrationConnectionUpdated @@ -2937,7 +2937,7 @@ export type V2Event1 = | ServerConnected | GlobalDisposed -export type V2Event = string +export type V2EventStream = string export type ForbiddenError = { _tag: "ForbiddenError" @@ -11901,7 +11901,7 @@ export type V2SessionEventsResponses = { 200: { id: string event: string - data: SessionDurableEvent1 + data: SessionDurableEventStream } } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index b0ed3df760..b30c3beb35 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -11260,7 +11260,7 @@ "type": "string" }, "data": { - "$ref": "#/components/schemas/SessionDurableEvent1" + "$ref": "#/components/schemas/SessionDurableEventStream" } }, "required": ["id", "event", "data"], @@ -23732,7 +23732,7 @@ "required": ["data", "hasMore"], "additionalProperties": false }, - "SessionDurableEvent1": { + "SessionDurableEventStream": { "type": "string", "contentSchema": { "$ref": "#/components/schemas/SessionDurableEvent" @@ -23975,7 +23975,7 @@ "required": ["id", "type", "data"], "additionalProperties": false }, - "V2Event1": { + "V2Event": { "anyOf": [ { "$ref": "#/components/schemas/Models-devRefreshed" @@ -24243,10 +24243,10 @@ } ] }, - "V2Event": { + "V2EventStream": { "type": "string", "contentSchema": { - "$ref": "#/components/schemas/V2Event1" + "$ref": "#/components/schemas/V2Event" }, "contentMediaType": "application/json" }, diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index c1dfcd6d7c..9b2e58907a 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -16,7 +16,7 @@ import type { SessionMessageAssistantTool, SessionV2Info, SkillV2Info, - V2Event1, + V2Event, } from "@opencode-ai/sdk/v2" import { createStore, produce } from "solid-js/store" import { createSimpleContext } from "./helper" @@ -121,7 +121,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ }, } - function handleEvent(event: V2Event1) { + function handleEvent(event: V2Event) { switch (event.type) { case "catalog.updated": void Promise.all([ @@ -408,7 +408,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ ...event, data: event.properties, location: { directory: metadata.directory, workspaceID: metadata.workspace }, - } as V2Event1) + } as V2Event) }) onCleanup(unsub) }) diff --git a/patches/effect@4.0.0-beta.83.patch b/patches/effect@4.0.0-beta.83.patch new file mode 100644 index 0000000000..11030bc6ab --- /dev/null +++ b/patches/effect@4.0.0-beta.83.patch @@ -0,0 +1,57 @@ +diff --git a/dist/unstable/httpapi/HttpApiSchema.js b/dist/unstable/httpapi/HttpApiSchema.js +index ecb6603d1ee4e89e92174b3a1100e8c9c720b3f3..08b832b6b3e9e4d10df156eca36b16f571f67f6e 100644 +--- a/dist/unstable/httpapi/HttpApiSchema.js ++++ b/dist/unstable/httpapi/HttpApiSchema.js +@@ -151,7 +151,7 @@ export const StreamSse = options => { + const events = options.events ?? (options.data === undefined ? undefined : Schema.Struct({ + id: Schema.UndefinedOr(Schema.String), + event: Schema.String, +- data: Schema.fromJsonString(options.data) ++ data: sseDataJsonSchema(options.data) + })); + if (events === undefined) { + throw new Error("StreamSse requires either an events schema or a data schema"); +@@ -166,6 +166,14 @@ export const StreamSse = options => { + error: options.error ?? Schema.Never + }); + }; ++const sseDataJsonSchema = data => { ++ const identifier = SchemaAST.resolveIdentifier(data.ast); ++ return identifier === undefined ? Schema.fromJsonString(data) : Schema.fromJsonString(data).annotate({ ++ // The SSE transport field is a JSON string. Give that wrapper its own ++ // OpenAPI identifier so it does not claim the decoded data schema's name. ++ identifier: `${identifier}Stream` ++ }); ++}; + /** + * Creates a streaming `Uint8Array` success response schema. + * +diff --git a/src/unstable/httpapi/HttpApiSchema.ts b/src/unstable/httpapi/HttpApiSchema.ts +index f2920365cf0328146523ce2e2dba04d6a8809a4a..5482d635cdf01a72063ff683d244fab891910211 100644 +--- a/src/unstable/httpapi/HttpApiSchema.ts ++++ b/src/unstable/httpapi/HttpApiSchema.ts +@@ -433,7 +433,7 @@ export const StreamSse: { + const events = options.events ?? (options.data === undefined ? undefined : Schema.Struct({ + id: Schema.UndefinedOr(Schema.String), + event: Schema.String, +- data: Schema.fromJsonString(options.data) ++ data: sseDataJsonSchema(options.data) + })) + if (events === undefined) { + throw new Error("StreamSse requires either an events schema or a data schema") +@@ -449,6 +449,15 @@ export const StreamSse: { + }) + } + ++const sseDataJsonSchema = (data: Schema.Constraint) => { ++ const identifier = SchemaAST.resolveIdentifier(data.ast) ++ return identifier === undefined ? Schema.fromJsonString(data) : Schema.fromJsonString(data).annotate({ ++ // The SSE transport field is a JSON string. Give that wrapper its own ++ // OpenAPI identifier so it does not claim the decoded data schema's name. ++ identifier: `${identifier}Stream` ++ }) ++} ++ + /** + * Creates a streaming `Uint8Array` success response schema. + *