diff --git a/packages/cli/src/services/daemon.ts b/packages/cli/src/services/daemon.ts index 4bf9c9e855..2e1f5bee4e 100644 --- a/packages/cli/src/services/daemon.ts +++ b/packages/cli/src/services/daemon.ts @@ -115,7 +115,8 @@ export const layer = Layer.effect( if (found) yield* stopProcess(found).pipe(Effect.ignore) const entrypoint = compiled ? undefined : process.argv[1] - if (!compiled && entrypoint === undefined) return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint")) + if (!compiled && entrypoint === undefined) + return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint")) yield* Effect.try({ try: () => { spawn(process.execPath, [...(entrypoint ? [entrypoint] : []), "serve", "--register"], { diff --git a/packages/cli/src/tui.ts b/packages/cli/src/tui.ts index bdb5bb6f30..3b8cc13db8 100644 --- a/packages/cli/src/tui.ts +++ b/packages/cli/src/tui.ts @@ -106,10 +106,13 @@ const legacyDefaults: Record = { "/config": {}, } -const gracefulFetch = Object.assign(async (input: RequestInfo | URL, init?: RequestInit) => { - const response = await fetch(input, init) - if (response.status !== 404) return response - const fallback = legacyDefaults[new URL(input instanceof Request ? input.url : input).pathname] - if (fallback === undefined) return response - return Response.json(fallback) -}, { preconnect: fetch.preconnect }) +const gracefulFetch = Object.assign( + async (input: RequestInfo | URL, init?: RequestInit) => { + const response = await fetch(input, init) + if (response.status !== 404) return response + const fallback = legacyDefaults[new URL(input instanceof Request ? input.url : input).pathname] + if (fallback === undefined) return response + return Response.json(fallback) + }, + { preconnect: fetch.preconnect }, +) diff --git a/packages/opencode/src/cli/tui/attention.ts b/packages/opencode/src/cli/tui/attention.ts index ecb42a3590..84d752f80a 100644 --- a/packages/opencode/src/cli/tui/attention.ts +++ b/packages/opencode/src/cli/tui/attention.ts @@ -100,9 +100,7 @@ function normalizePack(pack: TuiAttentionSoundPack): RegisteredSoundPack | undef sounds: Object.fromEntries( Object.entries(pack.sounds).filter( (item): item is [TuiAttentionSoundName, string] => - Schema.is(AttentionSoundName)(item[0]) && - typeof item[1] === "string" && - item[1].trim().length > 0, + Schema.is(AttentionSoundName)(item[0]) && typeof item[1] === "string" && item[1].trim().length > 0, ), ), } @@ -200,9 +198,7 @@ export function createTuiAttention(input: { const requestedSound = typeof request.sound === "object" ? request.sound : undefined const soundSkip = volume === undefined ? undefined : focusSkip(requestedSound?.when ?? "always", focus) const soundName = - requestedSound?.name && Schema.is(AttentionSoundName)(requestedSound.name) - ? requestedSound.name - : "default" + requestedSound?.name && Schema.is(AttentionSoundName)(requestedSound.name) ? requestedSound.name : "default" const sound = volume === undefined || soundSkip ? false : await playSound(soundName, volume) if (!notification && !sound) { diff --git a/packages/opencode/src/cli/tui/platform.ts b/packages/opencode/src/cli/tui/platform.ts index f9c617b4b6..001c81649c 100644 --- a/packages/opencode/src/cli/tui/platform.ts +++ b/packages/opencode/src/cli/tui/platform.ts @@ -86,13 +86,15 @@ export function discoverEditorConnection(directory: string) { : [] const score = Math.max(0, ...folders.map(contains)) if (!score) return [] - return [{ - url: `ws://127.0.0.1:${port}`, - authToken: typeof value.authToken === "string" ? value.authToken : undefined, - source: `lock:${port}`, - score, - mtime: statSync(file).mtimeMs, - }] + return [ + { + url: `ws://127.0.0.1:${port}`, + authToken: typeof value.authToken === "string" ? value.authToken : undefined, + source: `lock:${port}`, + score, + mtime: statSync(file).mtimeMs, + }, + ] } catch { return [] } @@ -110,11 +112,13 @@ function resolveZedDbPath() { path.join(os.homedir(), "Library", "Application Support", "Zed", "db", "0-stable", "db.sqlite"), path.join(os.homedir(), ".local", "share", "zed", "db", "0-stable", "db.sqlite"), ].filter((item): item is string => Boolean(item)) - return candidates.find((item) => { - try { - return statSync(item).isFile() - } catch { - return false - } - }) ?? "" + return ( + candidates.find((item) => { + try { + return statSync(item).isFile() + } catch { + return false + } + }) ?? "" + ) } diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts index 1850ef425c..61ce39ad39 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts @@ -4,7 +4,10 @@ import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "e import { HttpApiError, HttpApiMiddleware } from "effect/unstable/httpapi" import { hasPtyConnectTicketURL } from "@/server/shared/pty-ticket" import { isPublicUIPath } from "@/server/shared/public-ui" -export { Authorization as ServerAuthorization, authorizationLayer as serverAuthorizationLayer } from "@opencode-ai/server/middleware/authorization" +export { + Authorization as ServerAuthorization, + authorizationLayer as serverAuthorizationLayer, +} from "@opencode-ai/server/middleware/authorization" const AUTH_TOKEN_QUERY = "auth_token" const UNAUTHORIZED = 401 diff --git a/packages/opencode/test/cli/tui/editor-context-zed.test.ts b/packages/opencode/test/cli/tui/editor-context-zed.test.ts index 4973117334..860e13f9d3 100644 --- a/packages/opencode/test/cli/tui/editor-context-zed.test.ts +++ b/packages/opencode/test/cli/tui/editor-context-zed.test.ts @@ -3,12 +3,7 @@ import { mkdir, symlink } from "node:fs/promises" import os from "node:os" import path from "node:path" import { afterEach, expect, spyOn, test } from "bun:test" -import { - isZedTerminal, - offsetToPosition, - resolveZedDbPath, - resolveZedSelection, -} from "../../../src/cli/tui/editor-zed" +import { isZedTerminal, offsetToPosition, resolveZedDbPath, resolveZedSelection } from "../../../src/cli/tui/editor-zed" import { tmpdir } from "../../fixture/fixture" const originalZedTerm = process.env.ZED_TERM diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 9974138d2e..2895c47436 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -5400,6 +5400,64 @@ ] } }, + "/reference": { + "get": { + "tags": ["reference"], + "operationId": "reference.list", + "parameters": [ + { + "name": "directory", + "in": "query", + "schema": { + "type": "string" + }, + "required": false + }, + { + "name": "workspace", + "in": "query", + "schema": { + "type": "string" + }, + "required": false + } + ], + "responses": { + "200": { + "description": "Resolved configured references", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReferenceDescriptor" + }, + "description": "Resolved configured references" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "description": "List configured references resolved in the current workspace.", + "summary": "List configured references", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.reference.list({\n ...\n})" + } + ] + } + }, "/session": { "get": { "tags": ["session"], @@ -9904,7 +9962,7 @@ }, "/api/health": { "get": { - "tags": ["opencode experimental HttpApi"], + "tags": ["opencode HttpApi"], "operationId": "v2.health.get", "parameters": [], "security": [], @@ -9948,8 +10006,8 @@ } } }, - "description": "Check whether the v2 API server is ready to accept requests.", - "summary": "Check v2 server health", + "description": "Check whether the API server is ready to accept requests.", + "summary": "Check server health", "x-codeSamples": [ { "lang": "js", @@ -9960,7 +10018,7 @@ }, "/api/agent": { "get": { - "tags": ["opencode experimental HttpApi"], + "tags": ["opencode HttpApi"], "operationId": "v2.agent.list", "parameters": [ { @@ -10029,8 +10087,8 @@ } } }, - "description": "Retrieve currently registered v2 agents.", - "summary": "List v2 agents", + "description": "Retrieve currently registered agents.", + "summary": "List agents", "x-codeSamples": [ { "lang": "js", @@ -10041,7 +10099,7 @@ }, "/api/session": { "get": { - "tags": ["v2"], + "tags": ["sessions"], "operationId": "v2.session.list", "parameters": [ { @@ -10115,11 +10173,11 @@ "security": [], "responses": { "200": { - "description": "V2SessionsResponse", + "description": "SessionsResponse", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/V2SessionsResponse" + "$ref": "#/components/schemas/SessionsResponse" } } } @@ -10156,7 +10214,7 @@ } }, "description": "Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.", - "summary": "List v2 sessions", + "summary": "List sessions", "x-codeSamples": [ { "lang": "js", @@ -10167,7 +10225,7 @@ }, "/api/session/{sessionID}/prompt": { "post": { - "tags": ["v2"], + "tags": ["sessions"], "operationId": "v2.session.prompt", "parameters": [ { @@ -10224,7 +10282,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionNotFoundError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } @@ -10240,8 +10305,8 @@ } } }, - "description": "Durably admit one v2 session input and schedule agent-loop execution unless resume is false.", - "summary": "Send v2 message", + "description": "Durably admit one session input and schedule agent-loop execution unless resume is false.", + "summary": "Send message", "requestBody": { "content": { "application/json": { @@ -10280,7 +10345,7 @@ }, "/api/session/{sessionID}/compact": { "post": { - "tags": ["v2"], + "tags": ["sessions"], "operationId": "v2.session.compact", "parameters": [ { @@ -10323,7 +10388,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionNotFoundError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } @@ -10339,8 +10411,8 @@ } } }, - "description": "Compact a v2 session conversation.", - "summary": "Compact v2 session", + "description": "Compact a session conversation.", + "summary": "Compact session", "x-codeSamples": [ { "lang": "js", @@ -10351,7 +10423,7 @@ }, "/api/session/{sessionID}/wait": { "post": { - "tags": ["v2"], + "tags": ["sessions"], "operationId": "v2.session.wait", "parameters": [ { @@ -10394,7 +10466,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionNotFoundError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } @@ -10410,8 +10489,8 @@ } } }, - "description": "Wait for a v2 session agent loop to become idle.", - "summary": "Wait for v2 session", + "description": "Wait for a session agent loop to become idle.", + "summary": "Wait for session", "x-codeSamples": [ { "lang": "js", @@ -10422,7 +10501,7 @@ }, "/api/session/{sessionID}/context": { "get": { - "tags": ["v2"], + "tags": ["sessions"], "operationId": "v2.session.context", "parameters": [ { @@ -10482,7 +10561,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionNotFoundError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } @@ -10498,8 +10584,8 @@ } } }, - "description": "Retrieve the active context messages for a v2 session (all messages after the last compaction).", - "summary": "Get v2 session context", + "description": "Retrieve the active context messages for a session (all messages after the last compaction).", + "summary": "Get session context", "x-codeSamples": [ { "lang": "js", @@ -10510,7 +10596,7 @@ }, "/api/session/{sessionID}/message": { "get": { - "tags": ["v2 messages"], + "tags": ["messages"], "operationId": "v2.session.messages", "parameters": [ { @@ -10552,11 +10638,11 @@ "security": [], "responses": { "200": { - "description": "V2SessionMessagesResponse", + "description": "SessionMessagesResponse", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/V2SessionMessagesResponse" + "$ref": "#/components/schemas/SessionMessagesResponse" } } } @@ -10593,7 +10679,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionNotFoundError" + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] } } } @@ -10609,8 +10702,8 @@ } } }, - "description": "Retrieve projected v2 messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.", - "summary": "Get v2 session messages", + "description": "Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.", + "summary": "Get session messages", "x-codeSamples": [ { "lang": "js", @@ -10621,7 +10714,7 @@ }, "/api/model": { "get": { - "tags": ["v2 models"], + "tags": ["models"], "operationId": "v2.model.list", "parameters": [ { @@ -10700,8 +10793,8 @@ } } }, - "description": "Retrieve available v2 models ordered by release date.", - "summary": "List v2 models", + "description": "Retrieve available models ordered by release date.", + "summary": "List models", "x-codeSamples": [ { "lang": "js", @@ -10712,7 +10805,7 @@ }, "/api/provider": { "get": { - "tags": ["v2 providers"], + "tags": ["providers"], "operationId": "v2.provider.list", "parameters": [ { @@ -10791,8 +10884,8 @@ } } }, - "description": "Retrieve active v2 AI providers so clients can show provider availability and configuration.", - "summary": "List v2 providers", + "description": "Retrieve active AI providers so clients can show provider availability and configuration.", + "summary": "List providers", "x-codeSamples": [ { "lang": "js", @@ -10803,7 +10896,7 @@ }, "/api/provider/{providerID}": { "get": { - "tags": ["v2 providers"], + "tags": ["providers"], "operationId": "v2.provider.get", "parameters": [ { @@ -10897,8 +10990,8 @@ } } }, - "description": "Retrieve a single v2 AI provider so clients can inspect its availability and endpoint settings.", - "summary": "Get v2 provider", + "description": "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.", + "summary": "Get provider", "x-codeSamples": [ { "lang": "js", @@ -10909,7 +11002,7 @@ }, "/api/permission/request": { "get": { - "tags": ["v2 permissions"], + "tags": ["permissions"], "operationId": "v2.permission.request.list", "parameters": [ { @@ -10988,184 +11081,9 @@ ] } }, - "/api/session/{sessionID}/permission/request": { - "get": { - "tags": ["v2 session permissions"], - "operationId": "v2.session.permission.list", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionV2Request" - } - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionNotFoundError" - } - } - } - } - }, - "description": "Retrieve pending permission requests owned by a session.", - "summary": "List session permission requests", - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.list({\n ...\n})" - } - ] - } - }, - "/api/session/{sessionID}/permission/request/{requestID}/reply": { - "post": { - "tags": ["v2 session permissions"], - "operationId": "v2.session.permission.reply", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "requestID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^per" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | PermissionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/PermissionNotFoundError" - } - ] - } - } - } - } - }, - "description": "Respond to a pending permission request owned by a session.", - "summary": "Reply to pending permission request", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "reply": { - "$ref": "#/components/schemas/PermissionV2Reply" - }, - "message": { - "type": "string" - } - }, - "required": ["reply"], - "additionalProperties": false - } - } - }, - "required": true - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.reply({\n ...\n})" - } - ] - } - }, "/api/permission/saved": { "get": { - "tags": ["v2 saved permissions"], + "tags": ["permissions"], "operationId": "v2.permission.saved.list", "parameters": [ { @@ -11232,7 +11150,7 @@ }, "/api/permission/saved/{id}": { "delete": { - "tags": ["v2 saved permissions"], + "tags": ["permissions"], "operationId": "v2.permission.saved.remove", "parameters": [ { @@ -11280,9 +11198,194 @@ ] } }, + "/api/session/{sessionID}/permission": { + "get": { + "tags": ["permissions"], + "operationId": "v2.session.permission.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^ses" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionV2Request" + } + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending permission requests owned by a session.", + "summary": "List session permission requests", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.list({\n ...\n})" + } + ] + } + }, + "/api/session/{sessionID}/permission/{requestID}/reply": { + "post": { + "tags": ["permissions"], + "operationId": "v2.session.permission.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^ses" + }, + "required": true + }, + { + "name": "requestID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^per" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | PermissionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Respond to a pending permission request owned by a session.", + "summary": "Reply to pending permission request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reply": { + "$ref": "#/components/schemas/PermissionV2Reply" + }, + "message": { + "type": "string" + } + }, + "required": ["reply"], + "additionalProperties": false + } + } + }, + "required": true + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.reply({\n ...\n})" + } + ] + } + }, "/api/fs/read": { "get": { - "tags": ["v2 filesystem"], + "tags": ["filesystem"], "operationId": "v2.fs.read", "parameters": [ { @@ -11383,7 +11486,7 @@ }, "/api/fs/list": { "get": { - "tags": ["v2 filesystem"], + "tags": ["filesystem"], "operationId": "v2.fs.list", "parameters": [ { @@ -11480,7 +11583,7 @@ }, "/api/command": { "get": { - "tags": ["v2 commands"], + "tags": ["commands"], "operationId": "v2.command.list", "parameters": [ { @@ -11549,8 +11652,8 @@ } } }, - "description": "Retrieve currently registered v2 commands.", - "summary": "List v2 commands", + "description": "Retrieve currently registered commands.", + "summary": "List commands", "x-codeSamples": [ { "lang": "js", @@ -11561,7 +11664,7 @@ }, "/api/skill": { "get": { - "tags": ["v2 skills"], + "tags": ["skills"], "operationId": "v2.skill.list", "parameters": [ { @@ -11630,8 +11733,8 @@ } } }, - "description": "Retrieve currently registered v2 skills.", - "summary": "List v2 skills", + "description": "Retrieve currently registered skills.", + "summary": "List skills", "x-codeSamples": [ { "lang": "js", @@ -11642,7 +11745,7 @@ }, "/api/event": { "get": { - "tags": ["v2 events"], + "tags": ["events"], "operationId": "v2.event.subscribe", "parameters": [ { @@ -11698,8 +11801,8 @@ } } }, - "description": "Subscribe to native EventV2 payloads for a location.", - "summary": "Subscribe to v2 events", + "description": "Subscribe to native event payloads for a location.", + "summary": "Subscribe to events", "x-codeSamples": [ { "lang": "js", @@ -11710,7 +11813,7 @@ }, "/api/question/request": { "get": { - "tags": ["v2 questions"], + "tags": ["session questions"], "operationId": "v2.question.request.list", "parameters": [ { @@ -11789,9 +11892,9 @@ ] } }, - "/api/session/{sessionID}/question/request/{requestID}/reply": { + "/api/session/{sessionID}/question/{requestID}/reply": { "post": { - "tags": ["v2 session questions"], + "tags": ["session questions"], "operationId": "v2.session.question.reply", "parameters": [ { @@ -11844,11 +11947,14 @@ "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, { "$ref": "#/components/schemas/SessionNotFoundError" }, { - "$ref": "#/components/schemas/QuestionNotFoundError" + "$ref": "#/components/schemas/SessionNotFoundError" } ] } @@ -11876,9 +11982,9 @@ ] } }, - "/api/session/{sessionID}/question/request/{requestID}/reject": { + "/api/session/{sessionID}/question/{requestID}/reject": { "post": { - "tags": ["v2 session questions"], + "tags": ["session questions"], "operationId": "v2.session.question.reject", "parameters": [ { @@ -11931,11 +12037,14 @@ "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/QuestionNotFoundError" + }, { "$ref": "#/components/schemas/SessionNotFoundError" }, { - "$ref": "#/components/schemas/QuestionNotFoundError" + "$ref": "#/components/schemas/SessionNotFoundError" } ] } @@ -19932,6 +20041,70 @@ "required": ["name", "data"], "additionalProperties": false }, + "ReferenceDescriptor": { + "anyOf": [ + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": ["local"] + }, + "path": { + "type": "string" + } + }, + "required": ["name", "kind", "path"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": ["git"] + }, + "repository": { + "type": "string" + }, + "path": { + "type": "string" + }, + "branch": { + "type": "string" + } + }, + "required": ["name", "kind", "repository", "path"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": ["invalid"] + }, + "repository": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["name", "kind", "message"], + "additionalProperties": false + } + ] + }, "NotFoundError": { "type": "object", "required": ["name", "data"], @@ -20362,7 +20535,7 @@ "required": ["_tag", "message"], "additionalProperties": false }, - "V2SessionsResponse": { + "SessionsResponse": { "type": "object", "properties": { "data": { @@ -20469,7 +20642,7 @@ "required": ["_tag", "message"], "additionalProperties": false }, - "V2SessionMessagesResponse": { + "SessionMessagesResponse": { "type": "object", "properties": { "data": { @@ -28594,6 +28767,10 @@ "name": "provider", "description": "Experimental HttpApi provider routes." }, + { + "name": "reference", + "description": "Configured reference routes." + }, { "name": "session", "description": "Experimental HttpApi session routes." @@ -28611,64 +28788,52 @@ "description": "Experimental HttpApi workspace routes." }, { - "name": "opencode experimental HttpApi", + "name": "opencode HttpApi", "description": "Experimental HttpApi surface for selected instance routes." }, { - "name": "opencode experimental HttpApi", + "name": "opencode HttpApi", "description": "Experimental HttpApi surface for selected instance routes." }, { - "name": "v2", - "description": "Experimental v2 routes." + "name": "sessions", + "description": "Experimental session routes." }, { - "name": "v2 messages", - "description": "Experimental v2 message routes." + "name": "messages", + "description": "Experimental message routes." }, { - "name": "v2 models", - "description": "Experimental v2 model routes." + "name": "models", + "description": "Experimental model routes." }, { - "name": "v2 providers", - "description": "Experimental v2 provider routes." + "name": "providers", + "description": "Experimental provider routes." }, { - "name": "v2 permissions", - "description": "Experimental v2 permission routes." + "name": "permissions", + "description": "Experimental permission routes." }, { - "name": "v2 session permissions", - "description": "Experimental v2 session permission routes." + "name": "filesystem", + "description": "Experimental location-scoped filesystem routes." }, { - "name": "v2 saved permissions", - "description": "Experimental v2 saved permission routes." + "name": "commands", + "description": "Experimental command routes." }, { - "name": "v2 filesystem", - "description": "Experimental v2 location-scoped filesystem routes." + "name": "skills", + "description": "Experimental skill routes." }, { - "name": "v2 commands", - "description": "Experimental v2 command routes." + "name": "events", + "description": "Experimental event stream route." }, { - "name": "v2 skills", - "description": "Experimental v2 skill routes." - }, - { - "name": "v2 events", - "description": "Experimental v2 event stream route." - }, - { - "name": "v2 questions", - "description": "Experimental v2 question routes." - }, - { - "name": "v2 session questions", - "description": "Experimental v2 session question routes." + "name": "session questions", + "description": "Experimental session question routes." }, { "name": "pty", diff --git a/packages/server/src/groups/health.ts b/packages/server/src/groups/health.ts index 6e247480cb..18618164f0 100644 --- a/packages/server/src/groups/health.ts +++ b/packages/server/src/groups/health.ts @@ -1,15 +1,14 @@ import { Schema } from "effect" import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" -export const HealthGroup = HttpApiGroup.make("server.health") - .add( - HttpApiEndpoint.get("health.get", "/api/health", { - success: Schema.Struct({ healthy: Schema.Literal(true) }), - }).annotateMerge( - OpenApi.annotations({ - identifier: "v2.health.get", - summary: "Check server health", - description: "Check whether the API server is ready to accept requests.", - }), - ), - ) +export const HealthGroup = HttpApiGroup.make("server.health").add( + HttpApiEndpoint.get("health.get", "/api/health", { + success: Schema.Struct({ healthy: Schema.Literal(true) }), + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.health.get", + summary: "Check server health", + description: "Check whether the API server is ready to accept requests.", + }), + ), +) diff --git a/packages/server/src/groups/permission.ts b/packages/server/src/groups/permission.ts index 1bdb1d799d..a5b9e89a96 100644 --- a/packages/server/src/groups/permission.ts +++ b/packages/server/src/groups/permission.ts @@ -83,6 +83,4 @@ export const PermissionGroup = HttpApiGroup.make("server.permission") }), ), ) - .annotateMerge( - OpenApi.annotations({ title: "permissions", description: "Experimental permission routes." }), - ) + .annotateMerge(OpenApi.annotations({ title: "permissions", description: "Experimental permission routes." })) diff --git a/packages/server/src/groups/provider.ts b/packages/server/src/groups/provider.ts index 53a8be5bce..4b861ccc9e 100644 --- a/packages/server/src/groups/provider.ts +++ b/packages/server/src/groups/provider.ts @@ -33,8 +33,7 @@ export const ProviderGroup = HttpApiGroup.make("server.provider") OpenApi.annotations({ identifier: "v2.provider.get", summary: "Get provider", - description: - "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.", + description: "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.", }), ), ) diff --git a/packages/server/src/handlers/permission.ts b/packages/server/src/handlers/permission.ts index b3fefb166f..34fa3d428e 100644 --- a/packages/server/src/handlers/permission.ts +++ b/packages/server/src/handlers/permission.ts @@ -43,7 +43,11 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", "permission.saved.list", Effect.fn(function* (ctx) { const location = yield* Location.Service - return { data: yield* (yield* PermissionSaved.Service).list({ projectID: ctx.query.projectID ?? location.project.id }) } + return { + data: yield* (yield* PermissionSaved.Service).list({ + projectID: ctx.query.projectID ?? location.project.id, + }), + } }), ) .handle( diff --git a/packages/server/src/middleware/authorization.ts b/packages/server/src/middleware/authorization.ts index 7e31c82931..edbc85bb4f 100644 --- a/packages/server/src/middleware/authorization.ts +++ b/packages/server/src/middleware/authorization.ts @@ -7,12 +7,9 @@ import { HttpApiMiddleware } from "effect/unstable/httpapi" const AUTH_TOKEN_QUERY = "auth_token" const WWW_AUTHENTICATE = 'Basic realm="Secure Area"' -export class Authorization extends HttpApiMiddleware.Service()( - "@opencode/HttpApiAuthorization", - { - error: UnauthorizedError, - }, -) {} +export class Authorization extends HttpApiMiddleware.Service()("@opencode/HttpApiAuthorization", { + error: UnauthorizedError, +}) {} function emptyCredential() { return { username: "", password: Redacted.make("") } diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 6602088060..30d3999962 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -480,30 +480,33 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi const attention = props.host.attention({ renderer, config: tuiConfig, kv }) const platform = useTuiPlatform() - const api = createTuiApi(createTuiApiAdapters({ - version: build.version, - tuiConfig, - dialog, - keymap, - kv, - route, - routes: pluginRuntime.routes, - event, - sdk, - sync, - theme: themeState, - toast, - renderer, - attention, - Slot: pluginRuntime.Slot, - })) + const api = createTuiApi( + createTuiApiAdapters({ + version: build.version, + tuiConfig, + dialog, + keymap, + kv, + route, + routes: pluginRuntime.routes, + event, + sdk, + sync, + theme: themeState, + toast, + renderer, + attention, + Slot: pluginRuntime.Slot, + }), + ) const [ready, setReady] = createSignal(false) - props.pluginHost.start({ - api, - config: tuiConfig, - runtime: pluginRuntime, - dispose: () => attention.dispose(), - }) + props.pluginHost + .start({ + api, + config: tuiConfig, + runtime: pluginRuntime, + dispose: () => attention.dispose(), + }) .catch((error) => { console.error("Failed to load TUI plugins", error) }) @@ -529,7 +532,8 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi renderer.console.onCopySelection = async (text: string) => { if (!text || text.length === 0) return - await platform.clipboard?.write?.(text) + await platform.clipboard + ?.write?.(text) .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) .catch(toast.error) @@ -691,7 +695,8 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi run: async () => { const workspace = currentWorktreeWorkspace() if (!workspace?.directory) return - await platform.clipboard?.write?.(workspace.directory) + await platform.clipboard + ?.write?.(workspace.directory) .then(() => toast.show({ message: "Copied worktree path", variant: "info" })) .catch(toast.error) dialog.clear() diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index e37913eabc..df9239763a 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -16,10 +16,7 @@ export const AttentionSoundName = Schema.Literals([ export type AttentionSoundName = Schema.Schema.Type export const PluginOptions = Schema.Record(Schema.String, Schema.Unknown) -export const PluginSpec = Schema.Union([ - Schema.String, - Schema.mutable(Schema.Tuple([Schema.String, PluginOptions])), -]) +export const PluginSpec = Schema.Union([Schema.String, Schema.mutable(Schema.Tuple([Schema.String, PluginOptions]))]) export const LeaderTimeoutDefault = 2000 export const LeaderTimeout = Schema.Int.check(Schema.isGreaterThan(0)).annotate({ diff --git a/packages/tui/src/context/sync.tsx b/packages/tui/src/context/sync.tsx index 46a539ac75..42d732675e 100644 --- a/packages/tui/src/context/sync.tsx +++ b/packages/tui/src/context/sync.tsx @@ -55,7 +55,11 @@ export type SyncDependencies = { logger: { error(message: string, extra?: Record): void } } -export const { context: SyncContext, use: useSync, provider: SyncProvider } = createSimpleContext({ +export const { + context: SyncContext, + use: useSync, + provider: SyncProvider, +} = createSimpleContext({ name: "Sync", init: (dependencies: SyncDependencies) => { const environment = useTuiEnvironment() diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx index bbd9d7c156..690e7b5edc 100644 --- a/packages/tui/src/context/theme.tsx +++ b/packages/tui/src/context/theme.tsx @@ -96,14 +96,15 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ }) function syncCustomThemes() { - return (platform?.themes?.discover() ?? Promise.resolve({})).then((themes) => { - setCustomThemes( - Object.entries(themes).reduce>((result, [name, theme]) => { - if (isTheme(theme)) result[name] = theme - return result - }, {}), - ) - }) + return (platform?.themes?.discover() ?? Promise.resolve({})) + .then((themes) => { + setCustomThemes( + Object.entries(themes).reduce>((result, [name, theme]) => { + if (isTheme(theme)) result[name] = theme + return result + }, {}), + ) + }) .catch(() => setStore("active", "opencode")) } diff --git a/packages/tui/src/feature-plugins/home/tips-view.tsx b/packages/tui/src/feature-plugins/home/tips-view.tsx index 6dd0259143..979d7ec3a0 100644 --- a/packages/tui/src/feature-plugins/home/tips-view.tsx +++ b/packages/tui/src/feature-plugins/home/tips-view.tsx @@ -136,10 +136,12 @@ export function Tips(props: { api: TuiPluginApi; connected?: boolean }) { } const tip = createMemo(() => { if (props.connected === false) return NO_MODELS_TIP - const tips = [...TIPS, environment.capabilities.terminalSuspend ? TERMINAL_SUSPEND_TIP : INPUT_UNDO_TIP].flatMap((item) => { - const value = typeof item === "string" ? item : item(shortcuts) - return value ? [value] : [] - }) + const tips = [...TIPS, environment.capabilities.terminalSuspend ? TERMINAL_SUSPEND_TIP : INPUT_UNDO_TIP].flatMap( + (item) => { + const value = typeof item === "string" ? item : item(shortcuts) + return value ? [value] : [] + }, + ) return tips[Math.floor(tipOffset * tips.length)] ?? NO_MODELS_TIP }, NO_MODELS_TIP) // Solid can expose a memo's initial value while a pure computation is pending. diff --git a/packages/tui/src/keymap.tsx b/packages/tui/src/keymap.tsx index 92f63822f1..8d0aefc8f5 100644 --- a/packages/tui/src/keymap.tsx +++ b/packages/tui/src/keymap.tsx @@ -211,11 +211,7 @@ export function formatKeyBindings(bindings: Parameters((result, part) => { if (!isPastedTextPart(part)) return result return result.replace(part.source.text.value, part.text) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 01cd20398a..42efb847c3 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -460,7 +460,8 @@ export function Session() { }, run: async () => { const copy = (url: string) => - platform.clipboard?.write?.(url) + platform.clipboard + ?.write?.(url) .then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" })) .catch(() => toast.show({ message: "Failed to copy URL to clipboard", variant: "error" })) const url = session()?.share?.url @@ -895,7 +896,8 @@ export function Session() { return } - platform.clipboard?.write?.(text) + platform.clipboard + ?.write?.(text) .then(() => toast.show({ message: "Message copied to clipboard!", variant: "success" })) .catch(() => toast.show({ message: "Failed to copy to clipboard", variant: "error" })) dialog.clear() diff --git a/packages/tui/src/util/error.ts b/packages/tui/src/util/error.ts index 5d7f33f45c..757758a205 100644 --- a/packages/tui/src/util/error.ts +++ b/packages/tui/src/util/error.ts @@ -30,7 +30,8 @@ export function cliErrorMessage(input: unknown): string | undefined { } const provider = configData(input, "ProviderInitError") - if (provider) return `Failed to initialize provider "${field(provider, "providerID")}". Check credentials and configuration.` + if (provider) + return `Failed to initialize provider "${field(provider, "providerID")}". Check credentials and configuration.` const json = configData(input, "ConfigJsonError") if (json) { diff --git a/packages/tui/test/config.test.tsx b/packages/tui/test/config.test.tsx index 2820f0dacd..1ccb0986d1 100644 --- a/packages/tui/test/config.test.tsx +++ b/packages/tui/test/config.test.tsx @@ -20,14 +20,7 @@ test("defines package-owned plugin specs and attention sound names", () => { expect(decodePlugin("example-plugin")).toBe("example-plugin") expect(decodePlugin(["example-plugin", { enabled: true }])).toEqual(["example-plugin", { enabled: true }]) expect(() => decodePlugin(["example-plugin"])).toThrow() - expect(AttentionSoundName.literals).toEqual([ - "default", - "question", - "permission", - "error", - "done", - "subagent_done", - ]) + expect(AttentionSoundName.literals).toEqual(["default", "question", "permission", "error", "done", "subagent_done"]) }) test("validates config constraints", () => { diff --git a/packages/tui/test/fixture/tui-sdk.ts b/packages/tui/test/fixture/tui-sdk.ts index 780bcf32e8..f7e5bbe3fe 100644 --- a/packages/tui/test/fixture/tui-sdk.ts +++ b/packages/tui/test/fixture/tui-sdk.ts @@ -43,8 +43,19 @@ export function createFetch(override?: FetchHandler) { const overridden = await override?.(url) if (overridden) return overridden - if (["/agent", "/command", "/experimental/workspace", "/experimental/workspace/status", "/formatter", "/lsp"].includes(url.pathname)) return json([]) - if (["/config", "/experimental/resource", "/mcp", "/provider/auth", "/session/status"].includes(url.pathname)) return json({}) + if ( + [ + "/agent", + "/command", + "/experimental/workspace", + "/experimental/workspace/status", + "/formatter", + "/lsp", + ].includes(url.pathname) + ) + return json([]) + if (["/config", "/experimental/resource", "/mcp", "/provider/auth", "/session/status"].includes(url.pathname)) + return json({}) if (url.pathname === "/config/providers") return json({ providers: {}, default: {} }) if (url.pathname === "/experimental/console") return json({ consoleManagedProviders: [], switchableOrgCount: 0 }) if (url.pathname === "/path") return json({ home: "", state: "", config: "", worktree, directory }) diff --git a/packages/tui/test/keymap.test.tsx b/packages/tui/test/keymap.test.tsx index d99e46fd28..b0eed30812 100644 --- a/packages/tui/test/keymap.test.tsx +++ b/packages/tui/test/keymap.test.tsx @@ -5,12 +5,7 @@ import { testRender, useRenderer } from "@opentui/solid" import { expect, test } from "bun:test" import { onCleanup } from "solid-js" import { TuiKeybind } from "../src/config/keybind" -import { - getOpencodeModeStack, - OPENCODE_BASE_MODE, - OpencodeKeymapProvider, - registerOpencodeKeymap, -} from "../src/keymap" +import { getOpencodeModeStack, OPENCODE_BASE_MODE, OpencodeKeymapProvider, registerOpencodeKeymap } from "../src/keymap" function createResolvedKeymapConfig(input: TuiKeybind.KeybindOverrides = {}) { const keybinds = TuiKeybind.parse(input) diff --git a/packages/tui/test/util/transcript.test.ts b/packages/tui/test/util/transcript.test.ts index b364b5b7f7..02d6ef0bb9 100644 --- a/packages/tui/test/util/transcript.test.ts +++ b/packages/tui/test/util/transcript.test.ts @@ -1,10 +1,5 @@ import { describe, expect, test } from "bun:test" -import { - formatAssistantHeader, - formatMessage, - formatPart, - formatTranscript, -} from "../../src/util/transcript" +import { formatAssistantHeader, formatMessage, formatPart, formatTranscript } from "../../src/util/transcript" import type { AssistantMessage, Part, Provider, UserMessage } from "@opencode-ai/sdk/v2" const providers: Provider[] = [