Przeglądaj źródła

Merge branch 'dev' of https://github.com/sst/opencode into dev

David Hill 4 miesięcy temu
rodzic
commit
4a77e94e3c

+ 1 - 0
CONTRIBUTING.md

@@ -17,6 +17,7 @@ If you are unsure if a PR would be accepted, feel free to ask a maintainer or lo
 - [`help wanted`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
 - [`help wanted`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
 - [`good first issue`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
 - [`good first issue`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
 - [`bug`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
 - [`bug`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
+- [`perf`](https://github.com/sst/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
 
 
 > [!NOTE]
 > [!NOTE]
 > PRs that ignore these guardrails will likely be closed.
 > PRs that ignore these guardrails will likely be closed.

+ 11 - 11
bun.lock

@@ -37,7 +37,7 @@
     },
     },
     "packages/console/core": {
     "packages/console/core": {
       "name": "@opencode-ai/console-core",
       "name": "@opencode-ai/console-core",
-      "version": "0.15.23",
+      "version": "0.15.25",
       "dependencies": {
       "dependencies": {
         "@aws-sdk/client-sts": "3.782.0",
         "@aws-sdk/client-sts": "3.782.0",
         "@jsx-email/render": "1.1.1",
         "@jsx-email/render": "1.1.1",
@@ -64,7 +64,7 @@
     },
     },
     "packages/console/function": {
     "packages/console/function": {
       "name": "@opencode-ai/console-function",
       "name": "@opencode-ai/console-function",
-      "version": "0.15.23",
+      "version": "0.15.25",
       "dependencies": {
       "dependencies": {
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/openai": "2.0.2",
         "@ai-sdk/openai": "2.0.2",
@@ -88,7 +88,7 @@
     },
     },
     "packages/console/mail": {
     "packages/console/mail": {
       "name": "@opencode-ai/console-mail",
       "name": "@opencode-ai/console-mail",
-      "version": "0.15.23",
+      "version": "0.15.25",
       "dependencies": {
       "dependencies": {
         "@jsx-email/all": "2.2.3",
         "@jsx-email/all": "2.2.3",
         "@jsx-email/cli": "1.4.3",
         "@jsx-email/cli": "1.4.3",
@@ -109,7 +109,7 @@
     },
     },
     "packages/desktop": {
     "packages/desktop": {
       "name": "@opencode-ai/desktop",
       "name": "@opencode-ai/desktop",
-      "version": "0.15.23",
+      "version": "0.15.25",
       "dependencies": {
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
@@ -150,7 +150,7 @@
     },
     },
     "packages/function": {
     "packages/function": {
       "name": "@opencode-ai/function",
       "name": "@opencode-ai/function",
-      "version": "0.15.23",
+      "version": "0.15.25",
       "dependencies": {
       "dependencies": {
         "@octokit/auth-app": "8.0.1",
         "@octokit/auth-app": "8.0.1",
         "@octokit/rest": "22.0.0",
         "@octokit/rest": "22.0.0",
@@ -166,7 +166,7 @@
     },
     },
     "packages/opencode": {
     "packages/opencode": {
       "name": "opencode",
       "name": "opencode",
-      "version": "0.15.23",
+      "version": "0.15.25",
       "bin": {
       "bin": {
         "opencode": "./bin/opencode",
         "opencode": "./bin/opencode",
       },
       },
@@ -230,7 +230,7 @@
     },
     },
     "packages/plugin": {
     "packages/plugin": {
       "name": "@opencode-ai/plugin",
       "name": "@opencode-ai/plugin",
-      "version": "0.15.23",
+      "version": "0.15.25",
       "dependencies": {
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
         "zod": "catalog:",
         "zod": "catalog:",
@@ -250,7 +250,7 @@
     },
     },
     "packages/sdk/js": {
     "packages/sdk/js": {
       "name": "@opencode-ai/sdk",
       "name": "@opencode-ai/sdk",
-      "version": "0.15.23",
+      "version": "0.15.25",
       "devDependencies": {
       "devDependencies": {
         "@hey-api/openapi-ts": "0.81.0",
         "@hey-api/openapi-ts": "0.81.0",
         "@tsconfig/node22": "catalog:",
         "@tsconfig/node22": "catalog:",
@@ -261,7 +261,7 @@
     },
     },
     "packages/slack": {
     "packages/slack": {
       "name": "@opencode-ai/slack",
       "name": "@opencode-ai/slack",
-      "version": "0.15.23",
+      "version": "0.15.25",
       "dependencies": {
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
         "@slack/bolt": "^3.17.1",
         "@slack/bolt": "^3.17.1",
@@ -274,7 +274,7 @@
     },
     },
     "packages/ui": {
     "packages/ui": {
       "name": "@opencode-ai/ui",
       "name": "@opencode-ai/ui",
-      "version": "0.15.23",
+      "version": "0.15.25",
       "dependencies": {
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@kobalte/core": "catalog:",
         "@pierre/precision-diffs": "catalog:",
         "@pierre/precision-diffs": "catalog:",
@@ -297,7 +297,7 @@
     },
     },
     "packages/web": {
     "packages/web": {
       "name": "@opencode-ai/web",
       "name": "@opencode-ai/web",
-      "version": "0.15.23",
+      "version": "0.15.25",
       "dependencies": {
       "dependencies": {
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/markdown-remark": "6.3.1",
         "@astrojs/markdown-remark": "6.3.1",

+ 1 - 1
packages/console/app/package.json

@@ -7,7 +7,7 @@
     "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
     "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
     "build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
     "build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
     "start": "vinxi start",
     "start": "vinxi start",
-    "version": "0.15.23"
+    "version": "0.15.25"
   },
   },
   "dependencies": {
   "dependencies": {
     "@ibm/plex": "6.4.1",
     "@ibm/plex": "6.4.1",

+ 1 - 1
packages/console/core/package.json

@@ -1,7 +1,7 @@
 {
 {
   "$schema": "https://json.schemastore.org/package.json",
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@opencode-ai/console-core",
   "name": "@opencode-ai/console-core",
-  "version": "0.15.23",
+  "version": "0.15.25",
   "private": true,
   "private": true,
   "type": "module",
   "type": "module",
   "dependencies": {
   "dependencies": {

+ 1 - 1
packages/console/function/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@opencode-ai/console-function",
   "name": "@opencode-ai/console-function",
-  "version": "0.15.23",
+  "version": "0.15.25",
   "$schema": "https://json.schemastore.org/package.json",
   "$schema": "https://json.schemastore.org/package.json",
   "private": true,
   "private": true,
   "type": "module",
   "type": "module",

+ 1 - 1
packages/console/mail/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@opencode-ai/console-mail",
   "name": "@opencode-ai/console-mail",
-  "version": "0.15.23",
+  "version": "0.15.25",
   "dependencies": {
   "dependencies": {
     "@jsx-email/all": "2.2.3",
     "@jsx-email/all": "2.2.3",
     "@jsx-email/cli": "1.4.3",
     "@jsx-email/cli": "1.4.3",

+ 1 - 1
packages/desktop/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@opencode-ai/desktop",
   "name": "@opencode-ai/desktop",
-  "version": "0.15.23",
+  "version": "0.15.25",
   "description": "",
   "description": "",
   "type": "module",
   "type": "module",
   "scripts": {
   "scripts": {

+ 5 - 3
packages/desktop/src/components/prompt-input.tsx

@@ -393,9 +393,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                     <img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0 " />
                     <img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0 " />
                     <div class="flex gap-x-3 items-baseline flex-[1_0_0]">
                     <div class="flex gap-x-3 items-baseline flex-[1_0_0]">
                       <span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
                       <span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
-                      <span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
-                        {DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")}
-                      </span>
+                      <Show when={i.release_date}>
+                        <span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
+                          {DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")}
+                        </span>
+                      </Show>
                     </div>
                     </div>
                   </div>
                   </div>
                   <Show when={!i.cost || i.cost?.input === 0}>
                   <Show when={!i.cost || i.cost?.input === 0}>

+ 0 - 1
packages/desktop/src/context/local.tsx

@@ -481,7 +481,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
         if (!message) return ""
         if (!message) return ""
         if (Array.isArray(message)) return message.map((m) => getMessageText(m)).join(" ")
         if (Array.isArray(message)) return message.map((m) => getMessageText(m)).join(" ")
         const fileParts = sync.data.part[message.id]?.filter((p) => p.type === "file")
         const fileParts = sync.data.part[message.id]?.filter((p) => p.type === "file")
-        console.log(fileParts)
 
 
         return sync.data.part[message.id]
         return sync.data.part[message.id]
           ?.filter((p) => p.type === "text")
           ?.filter((p) => p.type === "text")

+ 1 - 1
packages/function/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@opencode-ai/function",
   "name": "@opencode-ai/function",
-  "version": "0.15.23",
+  "version": "0.15.25",
   "$schema": "https://json.schemastore.org/package.json",
   "$schema": "https://json.schemastore.org/package.json",
   "private": true,
   "private": true,
   "type": "module",
   "type": "module",

+ 1 - 1
packages/opencode/package.json

@@ -1,6 +1,6 @@
 {
 {
   "$schema": "https://json.schemastore.org/package.json",
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "0.15.23",
+  "version": "0.15.25",
   "name": "opencode",
   "name": "opencode",
   "type": "module",
   "type": "module",
   "private": true,
   "private": true,

+ 3 - 1
packages/opencode/src/installation/index.ts

@@ -142,7 +142,9 @@ export namespace Installation {
   export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}`
   export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}`
 
 
   export async function latest() {
   export async function latest() {
-    return fetch(`https://registry.npmjs.org/opencode-ai/${CHANNEL}`)
+    const [major] = VERSION.split(".").map((x) => Number(x))
+    const channel = CHANNEL === "latest" ? `latest-${major}` : CHANNEL
+    return fetch(`https://registry.npmjs.org/opencode-ai/${channel}`)
       .then((res) => {
       .then((res) => {
         if (!res.ok) throw new Error(res.statusText)
         if (!res.ok) throw new Error(res.statusText)
         return res.json()
         return res.json()

+ 46 - 23
packages/opencode/src/session/prompt.ts

@@ -286,7 +286,11 @@ export namespace SessionPrompt {
             OUTPUT_TOKEN_MAX,
             OUTPUT_TOKEN_MAX,
           ),
           ),
           abortSignal: abort.signal,
           abortSignal: abort.signal,
-          providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, params.options),
+          providerOptions: ProviderTransform.providerOptions(
+            model.npm,
+            model.providerID,
+            params.options,
+          ),
           stopWhen: stepCountIs(1),
           stopWhen: stepCountIs(1),
           temperature: params.temperature,
           temperature: params.temperature,
           topP: params.topP,
           topP: params.topP,
@@ -321,7 +325,11 @@ export namespace SessionPrompt {
                 async transformParams(args) {
                 async transformParams(args) {
                   if (args.type === "stream") {
                   if (args.type === "stream") {
                     // @ts-expect-error
                     // @ts-expect-error
-                    args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
+                    args.params.prompt = ProviderTransform.message(
+                      args.params.prompt,
+                      model.providerID,
+                      model.modelID,
+                    )
                   }
                   }
                   return args.params
                   return args.params
                 },
                 },
@@ -504,7 +512,11 @@ export namespace SessionPrompt {
     )
     )
     for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
     for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
       if (Wildcard.all(item.id, enabledTools) === false) continue
       if (Wildcard.all(item.id, enabledTools) === false) continue
-      const schema = ProviderTransform.schema(input.providerID, input.modelID, z.toJSONSchema(item.parameters))
+      const schema = ProviderTransform.schema(
+        input.providerID,
+        input.modelID,
+        z.toJSONSchema(item.parameters),
+      )
       tools[item.id] = tool({
       tools[item.id] = tool({
         id: item.id as any,
         id: item.id as any,
         description: item.description,
         description: item.description,
@@ -521,6 +533,7 @@ export namespace SessionPrompt {
               args,
               args,
             },
             },
           )
           )
+          item.parameters.parse(args)
           const result = await item.execute(args, {
           const result = await item.execute(args, {
             sessionID: input.sessionID,
             sessionID: input.sessionID,
             abort: options.abortSignal!,
             abort: options.abortSignal!,
@@ -585,17 +598,7 @@ export namespace SessionPrompt {
             args,
             args,
           },
           },
         )
         )
-        const result = await execute(args, opts).catch((err: unknown) => {
-          log.error("Error executing tool", { error: err, tool: key })
-          return {
-            content: [
-              {
-                type: "text",
-                text: `Failed to execute tool: ${err instanceof Error ? err.message : String(err)}`,
-              },
-            ],
-          }
-        })
+        const result = await execute(args, opts)
 
 
         await Plugin.trigger(
         await Plugin.trigger(
           "tool.execute.after",
           "tool.execute.after",
@@ -809,7 +812,9 @@ export namespace SessionPrompt {
                   messageID: info.id,
                   messageID: info.id,
                   sessionID: input.sessionID,
                   sessionID: input.sessionID,
                   type: "file",
                   type: "file",
-                  url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"),
+                  url:
+                    `data:${part.mime};base64,` +
+                    Buffer.from(await file.bytes()).toString("base64"),
                   mime: part.mime,
                   mime: part.mime,
                   filename: part.filename!,
                   filename: part.filename!,
                   source: part.source,
                   source: part.source,
@@ -883,7 +888,9 @@ export namespace SessionPrompt {
         synthetic: true,
         synthetic: true,
       })
       })
     }
     }
-    const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.mode === "plan")
+    const wasPlan = input.messages.some(
+      (msg) => msg.info.role === "assistant" && msg.info.mode === "plan",
+    )
     if (wasPlan && input.agent.name === "build") {
     if (wasPlan && input.agent.name === "build") {
       userMessage.parts.push({
       userMessage.parts.push({
         id: Identifier.ascending("part"),
         id: Identifier.ascending("part"),
@@ -963,7 +970,10 @@ export namespace SessionPrompt {
       partFromToolCall(toolCallID: string) {
       partFromToolCall(toolCallID: string) {
         return toolcalls[toolCallID]
         return toolcalls[toolCallID]
       },
       },
-      async process(stream: StreamTextResult<Record<string, AITool>, never>, retries: { count: number; max: number }) {
+      async process(
+        stream: StreamTextResult<Record<string, AITool>, never>,
+        retries: { count: number; max: number },
+      ) {
         log.info("process")
         log.info("process")
         if (!assistantMsg) throw new Error("call next() first before processing")
         if (!assistantMsg) throw new Error("call next() first before processing")
         let shouldRetry = false
         let shouldRetry = false
@@ -1094,7 +1104,10 @@ export namespace SessionPrompt {
                       status: "error",
                       status: "error",
                       input: value.input,
                       input: value.input,
                       error: (value.error as any).toString(),
                       error: (value.error as any).toString(),
-                      metadata: value.error instanceof Permission.RejectedError ? value.error.metadata : undefined,
+                      metadata:
+                        value.error instanceof Permission.RejectedError
+                          ? value.error.metadata
+                          : undefined,
                       time: {
                       time: {
                         start: match.state.time.start,
                         start: match.state.time.start,
                         end: Date.now(),
                         end: Date.now(),
@@ -1218,7 +1231,11 @@ export namespace SessionPrompt {
             error: e,
             error: e,
           })
           })
           const error = MessageV2.fromError(e, { providerID: input.providerID })
           const error = MessageV2.fromError(e, { providerID: input.providerID })
-          if (retries.count < retries.max && MessageV2.APIError.isInstance(error) && error.data.isRetryable) {
+          if (
+            retries.count < retries.max &&
+            MessageV2.APIError.isInstance(error) &&
+            error.data.isRetryable
+          ) {
             shouldRetry = true
             shouldRetry = true
             await Session.updatePart({
             await Session.updatePart({
               id: Identifier.ascending("part"),
               id: Identifier.ascending("part"),
@@ -1241,7 +1258,11 @@ export namespace SessionPrompt {
         }
         }
         const p = await Session.getParts(assistantMsg.id)
         const p = await Session.getParts(assistantMsg.id)
         for (const part of p) {
         for (const part of p) {
-          if (part.type === "tool" && part.state.status !== "completed" && part.state.status !== "error") {
+          if (
+            part.type === "tool" &&
+            part.state.status !== "completed" &&
+            part.state.status !== "error"
+          ) {
             Session.updatePart({
             Session.updatePart({
               ...part,
               ...part,
               state: {
               state: {
@@ -1705,11 +1726,13 @@ export namespace SessionPrompt {
     if (input.session.parentID) return
     if (input.session.parentID) return
     if (!Session.isDefaultTitle(input.session.title)) return
     if (!Session.isDefaultTitle(input.session.title)) return
     const isFirst =
     const isFirst =
-      input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic))
-        .length === 1
+      input.history.filter(
+        (m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic),
+      ).length === 1
     if (!isFirst) return
     if (!isFirst) return
     const small =
     const small =
-      (await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
+      (await Provider.getSmallModel(input.providerID)) ??
+      (await Provider.getModel(input.providerID, input.modelID))
     const options = {
     const options = {
       ...ProviderTransform.options(small.providerID, small.modelID, input.session.id),
       ...ProviderTransform.options(small.providerID, small.modelID, input.session.id),
       ...small.info.options,
       ...small.info.options,

+ 22 - 6
packages/opencode/src/session/summary.ts

@@ -29,7 +29,15 @@ export namespace SessionSummary {
   )
   )
 
 
   async function summarizeSession(input: { sessionID: string; messages: MessageV2.WithParts[] }) {
   async function summarizeSession(input: { sessionID: string; messages: MessageV2.WithParts[] }) {
-    const diffs = await computeDiff({ messages: input.messages })
+    const files = new Set(
+      input.messages
+        .flatMap((x) => x.parts)
+        .filter((x) => x.type === "patch")
+        .flatMap((x) => x.files),
+    )
+    const diffs = await computeDiff({ messages: input.messages }).then((x) =>
+      x.filter((x) => files.has(x.file)),
+    )
     await Session.update(input.sessionID, (draft) => {
     await Session.update(input.sessionID, (draft) => {
       draft.summary = {
       draft.summary = {
         diffs,
         diffs,
@@ -39,7 +47,9 @@ export namespace SessionSummary {
 
 
   async function summarizeMessage(input: { messageID: string; messages: MessageV2.WithParts[] }) {
   async function summarizeMessage(input: { messageID: string; messages: MessageV2.WithParts[] }) {
     const messages = input.messages.filter(
     const messages = input.messages.filter(
-      (m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
+      (m) =>
+        m.info.id === input.messageID ||
+        (m.info.role === "assistant" && m.info.parentID === input.messageID),
     )
     )
     const msgWithParts = messages.find((m) => m.info.id === input.messageID)!
     const msgWithParts = messages.find((m) => m.info.id === input.messageID)!
     const userMsg = msgWithParts.info as MessageV2.User
     const userMsg = msgWithParts.info as MessageV2.User
@@ -50,11 +60,14 @@ export namespace SessionSummary {
     }
     }
     await Session.updateMessage(userMsg)
     await Session.updateMessage(userMsg)
 
 
-    const assistantMsg = messages.find((m) => m.info.role === "assistant")!.info as MessageV2.Assistant
+    const assistantMsg = messages.find((m) => m.info.role === "assistant")!
+      .info as MessageV2.Assistant
     const small = await Provider.getSmallModel(assistantMsg.providerID)
     const small = await Provider.getSmallModel(assistantMsg.providerID)
     if (!small) return
     if (!small) return
 
 
-    const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart
+    const textPart = msgWithParts.parts.find(
+      (p) => p.type === "text" && !p.synthetic,
+    ) as MessageV2.TextPart
     if (textPart && !userMsg.summary?.title) {
     if (textPart && !userMsg.summary?.title) {
       const result = await generateText({
       const result = await generateText({
         maxOutputTokens: small.info.reasoning ? 1500 : 20,
         maxOutputTokens: small.info.reasoning ? 1500 : 20,
@@ -81,7 +94,8 @@ export namespace SessionSummary {
     if (
     if (
       messages.some(
       messages.some(
         (m) =>
         (m) =>
-          m.info.role === "assistant" && m.parts.some((p) => p.type === "step-finish" && p.reason !== "tool-calls"),
+          m.info.role === "assistant" &&
+          m.parts.some((p) => p.type === "step-finish" && p.reason !== "tool-calls"),
       )
       )
     ) {
     ) {
       const result = await generateText({
       const result = await generateText({
@@ -114,7 +128,9 @@ export namespace SessionSummary {
       let all = await Session.messages(input.sessionID)
       let all = await Session.messages(input.sessionID)
       if (input.messageID)
       if (input.messageID)
         all = all.filter(
         all = all.filter(
-          (x) => x.info.id === input.messageID || (x.info.role === "assistant" && x.info.parentID === input.messageID),
+          (x) =>
+            x.info.id === input.messageID ||
+            (x.info.role === "assistant" && x.info.parentID === input.messageID),
         )
         )
 
 
       return computeDiff({
       return computeDiff({

+ 1 - 1
packages/plugin/package.json

@@ -1,7 +1,7 @@
 {
 {
   "$schema": "https://json.schemastore.org/package.json",
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@opencode-ai/plugin",
   "name": "@opencode-ai/plugin",
-  "version": "0.15.23",
+  "version": "0.15.25",
   "type": "module",
   "type": "module",
   "scripts": {
   "scripts": {
     "typecheck": "tsgo --noEmit",
     "typecheck": "tsgo --noEmit",

+ 1 - 1
packages/sdk/js/package.json

@@ -1,7 +1,7 @@
 {
 {
   "$schema": "https://json.schemastore.org/package.json",
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@opencode-ai/sdk",
   "name": "@opencode-ai/sdk",
-  "version": "0.15.23",
+  "version": "0.15.25",
   "type": "module",
   "type": "module",
   "scripts": {
   "scripts": {
     "typecheck": "tsgo --noEmit",
     "typecheck": "tsgo --noEmit",

+ 39 - 0
packages/sdk/js/src/gen/sdk.gen.ts

@@ -125,6 +125,10 @@ import type {
   TuiExecuteCommandErrors,
   TuiExecuteCommandErrors,
   TuiShowToastData,
   TuiShowToastData,
   TuiShowToastResponses,
   TuiShowToastResponses,
+  TuiControlNextData,
+  TuiControlNextResponses,
+  TuiControlResponseData,
+  TuiControlResponseResponses,
   AuthSetData,
   AuthSetData,
   AuthSetResponses,
   AuthSetResponses,
   AuthSetErrors,
   AuthSetErrors,
@@ -750,6 +754,40 @@ class Mcp extends _HeyApiClient {
   }
   }
 }
 }
 
 
+class Control extends _HeyApiClient {
+  /**
+   * Get the next TUI request from the queue
+   */
+  public next<ThrowOnError extends boolean = false>(
+    options?: Options<TuiControlNextData, ThrowOnError>,
+  ) {
+    return (options?.client ?? this._client).get<TuiControlNextResponses, unknown, ThrowOnError>({
+      url: "/tui/control/next",
+      ...options,
+    })
+  }
+
+  /**
+   * Submit a response to the TUI request queue
+   */
+  public response<ThrowOnError extends boolean = false>(
+    options?: Options<TuiControlResponseData, ThrowOnError>,
+  ) {
+    return (options?.client ?? this._client).post<
+      TuiControlResponseResponses,
+      unknown,
+      ThrowOnError
+    >({
+      url: "/tui/control/response",
+      ...options,
+      headers: {
+        "Content-Type": "application/json",
+        ...options?.headers,
+      },
+    })
+  }
+}
+
 class Tui extends _HeyApiClient {
 class Tui extends _HeyApiClient {
   /**
   /**
    * Append prompt to the TUI
    * Append prompt to the TUI
@@ -878,6 +916,7 @@ class Tui extends _HeyApiClient {
       },
       },
     })
     })
   }
   }
+  control = new Control({ client: this._client })
 }
 }
 
 
 class Auth extends _HeyApiClient {
 class Auth extends _HeyApiClient {

+ 40 - 0
packages/sdk/js/src/gen/types.gen.ts

@@ -2632,6 +2632,46 @@ export type TuiShowToastResponses = {
 
 
 export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses]
 export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses]
 
 
+export type TuiControlNextData = {
+  body?: never
+  path?: never
+  query?: {
+    directory?: string
+  }
+  url: "/tui/control/next"
+}
+
+export type TuiControlNextResponses = {
+  /**
+   * Next TUI request
+   */
+  200: {
+    path: string
+    body: unknown
+  }
+}
+
+export type TuiControlNextResponse = TuiControlNextResponses[keyof TuiControlNextResponses]
+
+export type TuiControlResponseData = {
+  body?: unknown
+  path?: never
+  query?: {
+    directory?: string
+  }
+  url: "/tui/control/response"
+}
+
+export type TuiControlResponseResponses = {
+  /**
+   * Response submitted successfully
+   */
+  200: boolean
+}
+
+export type TuiControlResponseResponse =
+  TuiControlResponseResponses[keyof TuiControlResponseResponses]
+
 export type AuthSetData = {
 export type AuthSetData = {
   body?: Auth
   body?: Auth
   path: {
   path: {

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@opencode-ai/slack",
   "name": "@opencode-ai/slack",
-  "version": "0.15.23",
+  "version": "0.15.25",
   "type": "module",
   "type": "module",
   "scripts": {
   "scripts": {
     "dev": "bun run src/index.ts",
     "dev": "bun run src/index.ts",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@opencode-ai/ui",
   "name": "@opencode-ai/ui",
-  "version": "0.15.23",
+  "version": "0.15.25",
   "type": "module",
   "type": "module",
   "exports": {
   "exports": {
     ".": "./src/components/index.ts",
     ".": "./src/components/index.ts",

+ 1 - 1
packages/web/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@opencode-ai/web",
   "name": "@opencode-ai/web",
   "type": "module",
   "type": "module",
-  "version": "0.15.23",
+  "version": "0.15.25",
   "scripts": {
   "scripts": {
     "dev": "astro dev",
     "dev": "astro dev",
     "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
     "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

+ 1 - 1
sdks/vscode/package.json

@@ -2,7 +2,7 @@
   "name": "opencode",
   "name": "opencode",
   "displayName": "opencode",
   "displayName": "opencode",
   "description": "opencode for VS Code",
   "description": "opencode for VS Code",
-  "version": "0.15.23",
+  "version": "0.15.25",
   "publisher": "sst-dev",
   "publisher": "sst-dev",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",