opencode-agent[bot] 2 mesi fa
parent
commit
155e1f20d6

+ 2 - 1
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"], {

+ 10 - 7
packages/cli/src/tui.ts

@@ -106,10 +106,13 @@ const legacyDefaults: Record<string, unknown> = {
   "/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 },
+)

+ 2 - 6
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) {

+ 18 - 14
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
+      }
+    }) ?? ""
+  )
 }

+ 4 - 1
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

+ 1 - 6
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

+ 359 - 194
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,19 +11081,18 @@
         ]
       }
     },
-    "/api/session/{sessionID}/permission/request": {
+    "/api/permission/saved": {
       "get": {
-        "tags": ["v2 session permissions"],
-        "operationId": "v2.session.permission.list",
+        "tags": ["permissions"],
+        "operationId": "v2.permission.saved.list",
         "parameters": [
           {
-            "name": "sessionID",
-            "in": "path",
+            "name": "projectID",
+            "in": "query",
             "schema": {
-              "type": "string",
-              "pattern": "^ses"
+              "type": "string"
             },
-            "required": true
+            "required": false
           }
         ],
         "security": [],
@@ -11015,7 +11107,7 @@
                     "data": {
                       "type": "array",
                       "items": {
-                        "$ref": "#/components/schemas/PermissionV2Request"
+                        "$ref": "#/components/schemas/PermissionSavedInfo"
                       }
                     }
                   },
@@ -11044,48 +11136,28 @@
                 }
               }
             }
-          },
-          "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",
+        "description": "Retrieve saved permissions, optionally filtered by project.",
+        "summary": "List saved permissions",
         "x-codeSamples": [
           {
             "lang": "js",
-            "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.list({\n  ...\n})"
+            "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.permission.saved.list({\n  ...\n})"
           }
         ]
       }
     },
-    "/api/session/{sessionID}/permission/request/{requestID}/reply": {
-      "post": {
-        "tags": ["v2 session permissions"],
-        "operationId": "v2.session.permission.reply",
+    "/api/permission/saved/{id}": {
+      "delete": {
+        "tags": ["permissions"],
+        "operationId": "v2.permission.saved.remove",
         "parameters": [
           {
-            "name": "sessionID",
-            "in": "path",
-            "schema": {
-              "type": "string",
-              "pattern": "^ses"
-            },
-            "required": true
-          },
-          {
-            "name": "requestID",
+            "name": "id",
             "in": "path",
             "schema": {
-              "type": "string",
-              "pattern": "^per"
+              "type": "string"
             },
             "required": true
           }
@@ -11114,67 +11186,31 @@
                 }
               }
             }
-          },
-          "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
-        },
+        "description": "Remove a saved permission by ID.",
+        "summary": "Remove saved permission",
         "x-codeSamples": [
           {
             "lang": "js",
-            "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.reply({\n  ...\n})"
+            "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.permission.saved.remove({\n  ...\n})"
           }
         ]
       }
     },
-    "/api/permission/saved": {
+    "/api/session/{sessionID}/permission": {
       "get": {
-        "tags": ["v2 saved permissions"],
-        "operationId": "v2.permission.saved.list",
+        "tags": ["permissions"],
+        "operationId": "v2.session.permission.list",
         "parameters": [
           {
-            "name": "projectID",
-            "in": "query",
+            "name": "sessionID",
+            "in": "path",
             "schema": {
-              "type": "string"
+              "type": "string",
+              "pattern": "^ses"
             },
-            "required": false
+            "required": true
           }
         ],
         "security": [],
@@ -11189,7 +11225,7 @@
                     "data": {
                       "type": "array",
                       "items": {
-                        "$ref": "#/components/schemas/PermissionSavedInfo"
+                        "$ref": "#/components/schemas/PermissionV2Request"
                       }
                     }
                   },
@@ -11218,28 +11254,55 @@
                 }
               }
             }
+          },
+          "404": {
+            "description": "SessionNotFoundError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "anyOf": [
+                    {
+                      "$ref": "#/components/schemas/SessionNotFoundError"
+                    },
+                    {
+                      "$ref": "#/components/schemas/SessionNotFoundError"
+                    }
+                  ]
+                }
+              }
+            }
           }
         },
-        "description": "Retrieve saved permissions, optionally filtered by project.",
-        "summary": "List saved permissions",
+        "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.permission.saved.list({\n  ...\n})"
+            "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.permission.list({\n  ...\n})"
           }
         ]
       }
     },
-    "/api/permission/saved/{id}": {
-      "delete": {
-        "tags": ["v2 saved permissions"],
-        "operationId": "v2.permission.saved.remove",
+    "/api/session/{sessionID}/permission/{requestID}/reply": {
+      "post": {
+        "tags": ["permissions"],
+        "operationId": "v2.session.permission.reply",
         "parameters": [
           {
-            "name": "id",
+            "name": "sessionID",
             "in": "path",
             "schema": {
-              "type": "string"
+              "type": "string",
+              "pattern": "^ses"
+            },
+            "required": true
+          },
+          {
+            "name": "requestID",
+            "in": "path",
+            "schema": {
+              "type": "string",
+              "pattern": "^per"
             },
             "required": true
           }
@@ -11268,21 +11331,61 @@
                 }
               }
             }
+          },
+          "404": {
+            "description": "SessionNotFoundError | PermissionNotFoundError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "anyOf": [
+                    {
+                      "$ref": "#/components/schemas/PermissionNotFoundError"
+                    },
+                    {
+                      "$ref": "#/components/schemas/SessionNotFoundError"
+                    },
+                    {
+                      "$ref": "#/components/schemas/SessionNotFoundError"
+                    }
+                  ]
+                }
+              }
+            }
           }
         },
-        "description": "Remove a saved permission by ID.",
-        "summary": "Remove saved permission",
+        "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.permission.saved.remove({\n  ...\n})"
+            "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": "v2 messages",
-      "description": "Experimental v2 message routes."
-    },
-    {
-      "name": "v2 models",
-      "description": "Experimental v2 model routes."
-    },
-    {
-      "name": "v2 providers",
-      "description": "Experimental v2 provider routes."
+      "name": "sessions",
+      "description": "Experimental session routes."
     },
     {
-      "name": "v2 permissions",
-      "description": "Experimental v2 permission routes."
+      "name": "messages",
+      "description": "Experimental message routes."
     },
     {
-      "name": "v2 session permissions",
-      "description": "Experimental v2 session permission routes."
+      "name": "models",
+      "description": "Experimental model routes."
     },
     {
-      "name": "v2 saved permissions",
-      "description": "Experimental v2 saved permission routes."
+      "name": "providers",
+      "description": "Experimental provider routes."
     },
     {
-      "name": "v2 filesystem",
-      "description": "Experimental v2 location-scoped filesystem routes."
+      "name": "permissions",
+      "description": "Experimental permission routes."
     },
     {
-      "name": "v2 commands",
-      "description": "Experimental v2 command routes."
+      "name": "filesystem",
+      "description": "Experimental location-scoped filesystem routes."
     },
     {
-      "name": "v2 skills",
-      "description": "Experimental v2 skill routes."
+      "name": "commands",
+      "description": "Experimental command routes."
     },
     {
-      "name": "v2 events",
-      "description": "Experimental v2 event stream route."
+      "name": "skills",
+      "description": "Experimental skill routes."
     },
     {
-      "name": "v2 questions",
-      "description": "Experimental v2 question routes."
+      "name": "events",
+      "description": "Experimental event stream route."
     },
     {
-      "name": "v2 session questions",
-      "description": "Experimental v2 session question routes."
+      "name": "session questions",
+      "description": "Experimental session question routes."
     },
     {
       "name": "pty",

+ 11 - 12
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.",
+    }),
+  ),
+)

+ 1 - 3
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." }))

+ 1 - 2
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.",
         }),
       ),
   )

+ 5 - 1
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(

+ 3 - 6
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<Authorization>()(
-  "@opencode/HttpApiAuthorization",
-  {
-    error: UnauthorizedError,
-  },
-) {}
+export class Authorization extends HttpApiMiddleware.Service<Authorization>()("@opencode/HttpApiAuthorization", {
+  error: UnauthorizedError,
+}) {}
 
 function emptyCredential() {
   return { username: "", password: Redacted.make("") }

+ 30 - 25
packages/tui/src/app.tsx

@@ -480,30 +480,33 @@ function App(props: { onSnapshot?: () => Promise<string[]>; 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<string[]>; 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<string[]>; 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()

+ 1 - 4
packages/tui/src/config/index.tsx

@@ -16,10 +16,7 @@ export const AttentionSoundName = Schema.Literals([
 export type AttentionSoundName = Schema.Schema.Type<typeof AttentionSoundName>
 
 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({

+ 5 - 1
packages/tui/src/context/sync.tsx

@@ -55,7 +55,11 @@ export type SyncDependencies = {
   logger: { error(message: string, extra?: Record<string, unknown>): 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()

+ 9 - 8
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<Record<string, ThemeJson>>((result, [name, theme]) => {
-            if (isTheme(theme)) result[name] = theme
-            return result
-          }, {}),
-        )
-      })
+      return (platform?.themes?.discover() ?? Promise.resolve({}))
+        .then((themes) => {
+          setCustomThemes(
+            Object.entries(themes).reduce<Record<string, ThemeJson>>((result, [name, theme]) => {
+              if (isTheme(theme)) result[name] = theme
+              return result
+            }, {}),
+          )
+        })
         .catch(() => setStore("active", "opencode"))
     }
 

+ 6 - 4
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.

+ 1 - 5
packages/tui/src/keymap.tsx

@@ -211,11 +211,7 @@ export function formatKeyBindings(bindings: Parameters<typeof formatCommandBindi
   return formatCommandBindingsExtra(bindings, formatOptions(config))
 }
 
-export function registerOpencodeKeymap(
-  keymap: OpenTuiKeymap,
-  renderer: CliRenderer,
-  config: ResolvedKeymapConfig,
-) {
+export function registerOpencodeKeymap(keymap: OpenTuiKeymap, renderer: CliRenderer, config: ResolvedKeymapConfig) {
   const modeStack = createOpencodeModeStack(keymap)
   const offCommaBindings = registerCommaBindings(keymap)
   const offAliasExpander = registerKeyAliases(keymap)

+ 1 - 4
packages/tui/src/prompt/part.ts

@@ -5,10 +5,7 @@ export function stripPromptPartIDs<Part extends { id: string; messageID: string;
   return rest
 }
 
-export function expandPastedTextPlaceholders(
-  text: string,
-  parts: readonly unknown[],
-) {
+export function expandPastedTextPlaceholders(text: string, parts: readonly unknown[]) {
   return parts.reduce<string>((result, part) => {
     if (!isPastedTextPart(part)) return result
     return result.replace(part.source.text.value, part.text)

+ 4 - 2
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()

+ 2 - 1
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) {

+ 1 - 8
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", () => {

+ 13 - 2
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 })

+ 1 - 6
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)

+ 1 - 6
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[] = [