From 733a3bd031203b9081decae7136d7c9f3ecffdca Mon Sep 17 00:00:00 2001 From: Valentin Vivaldi Date: Wed, 1 Apr 2026 23:34:01 -0300 Subject: [PATCH 01/19] fix(core): prevent agent loop from stopping after tool calls with OpenAI-compatible providers (#14973) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline --- packages/opencode/src/session/prompt.ts | 9 +++++ .../test/session/prompt-effect.test.ts | 33 +++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index fb4705603e3..436847ed4e3 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1362,9 +1362,18 @@ NOTE: At any point in time through this workflow you should feel free to ask the } if (!lastUser) throw new Error("No user message found in stream. This should never happen.") + + const lastAssistantMsg = msgs.findLast( + (msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id, + ) + // Some providers return "stop" even when the assistant message contains tool calls. + // Keep the loop running so tool results can be sent back to the model. + const hasToolCalls = lastAssistantMsg?.parts.some((part) => part.type === "tool") ?? false + if ( lastAssistant?.finish && !["tool-calls"].includes(lastAssistant.finish) && + !hasToolCalls && lastUser.id < lastAssistant.id ) { log.info("exiting loop", { sessionID }) diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 8e4543c2474..d077f26d6b7 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -3,7 +3,6 @@ import { expect, spyOn } from "bun:test" import { Cause, Effect, Exit, Fiber, Layer } from "effect" import path from "path" import z from "zod" -import type { Agent } from "../../src/agent/agent" import { Agent as AgentSvc } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { Command } from "../../src/command" @@ -35,7 +34,7 @@ import { Log } from "../../src/util/log" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture" import { testEffect } from "../lib/effect" -import { TestLLMServer } from "../lib/llm-server" +import { reply, TestLLMServer } from "../lib/llm-server" Log.init({ print: false }) @@ -453,6 +452,36 @@ it.live("loop continues when finish is tool-calls", () => ), ) +it.live("loop continues when finish is stop but assistant has tool parts", () => + provideTmpdirServer( + Effect.fnUntraced(function* ({ llm }) { + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const session = yield* sessions.create({ + title: "Pinned", + permission: [{ permission: "*", pattern: "*", action: "allow" }], + }) + yield* prompt.prompt({ + sessionID: session.id, + agent: "build", + noReply: true, + parts: [{ type: "text", text: "hello" }], + }) + yield* llm.push(reply().tool("first", { value: "first" }).stop()) + yield* llm.text("second") + + const result = yield* prompt.loop({ sessionID: session.id }) + expect(yield* llm.calls).toBe(2) + expect(result.info.role).toBe("assistant") + if (result.info.role === "assistant") { + expect(result.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true) + expect(result.info.finish).toBe("stop") + } + }), + { git: true, config: providerCfg }, + ), +) + it.live("failed subtask preserves metadata on error tool state", () => provideTmpdirServer( Effect.fnUntraced(function* ({ llm }) { From 5daf2fa7f028f21ba79fd36ec2eb8b0603032673 Mon Sep 17 00:00:00 2001 From: Aaron Zhu <139607425+aaron-he-zhu@users.noreply.github.com> Date: Thu, 2 Apr 2026 10:44:16 +0800 Subject: [PATCH 02/19] fix(session): compaction agent responds in same language as conversation (#20581) Co-authored-by: Aaron Zhu --- packages/opencode/src/agent/prompt/compaction.txt | 1 + packages/opencode/src/session/compaction.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/opencode/src/agent/prompt/compaction.txt b/packages/opencode/src/agent/prompt/compaction.txt index 3308627e153..11deccb3afc 100644 --- a/packages/opencode/src/agent/prompt/compaction.txt +++ b/packages/opencode/src/agent/prompt/compaction.txt @@ -12,3 +12,4 @@ Focus on information that would be helpful for continuing the conversation, incl Your summary should be comprehensive enough to provide context but concise enough to be quickly understood. Do not respond to any questions in the conversation, only output the summary. +Respond in the same language the user used in the conversation. diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index e48b1c7b08e..3158393f114 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -190,6 +190,7 @@ export namespace SessionCompaction { Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next. The summary that you construct will be used so that another agent can read it and continue the work. Do not call any tools. Respond only with the summary text. +Respond in the same language as the user's messages in the conversation. When constructing the summary, try to stick to this template: --- From 916afb5220d064990d846f4080a683762069fa25 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 1 Apr 2026 22:57:45 -0400 Subject: [PATCH 03/19] refactor(account): share token freshness helper (#20591) --- packages/opencode/src/account/index.ts | 8 ++++++-- packages/opencode/test/account/service.test.ts | 11 +++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/account/index.ts b/packages/opencode/src/account/index.ts index 947fadad07c..bcc90b7b1d5 100644 --- a/packages/opencode/src/account/index.ts +++ b/packages/opencode/src/account/index.ts @@ -120,6 +120,10 @@ class TokenRefreshRequest extends Schema.Class("TokenRefres const clientId = "opencode-cli" const eagerRefreshThreshold = Duration.minutes(5) +const eagerRefreshThresholdMs = Duration.toMillis(eagerRefreshThreshold) + +const isTokenFresh = (tokenExpiry: number | null, now: number) => + tokenExpiry != null && tokenExpiry > now + eagerRefreshThresholdMs const mapAccountServiceError = (message = "Account service operation failed") => @@ -219,7 +223,7 @@ export namespace Account { const account = maybeAccount.value const now = yield* Clock.currentTimeMillis - if (account.token_expiry && account.token_expiry > now + Duration.toMillis(eagerRefreshThreshold)) { + if (isTokenFresh(account.token_expiry, now)) { return account.access_token } @@ -229,7 +233,7 @@ export namespace Account { const resolveToken = Effect.fnUntraced(function* (row: AccountRow) { const now = yield* Clock.currentTimeMillis - if (row.token_expiry && row.token_expiry > now + Duration.toMillis(eagerRefreshThreshold)) { + if (isTokenFresh(row.token_expiry, now)) { return row.access_token } diff --git a/packages/opencode/test/account/service.test.ts b/packages/opencode/test/account/service.test.ts index e4c43a1f64f..85ab259f1da 100644 --- a/packages/opencode/test/account/service.test.ts +++ b/packages/opencode/test/account/service.test.ts @@ -18,6 +18,9 @@ const truncate = Layer.effectDiscard( const it = testEffect(Layer.merge(AccountRepo.layer, truncate)) +const insideEagerRefreshWindow = Duration.toMillis(Duration.minutes(1)) +const outsideEagerRefreshWindow = Duration.toMillis(Duration.minutes(10)) + const live = (client: HttpClient.HttpClient) => Account.layer.pipe(Layer.provide(Layer.succeed(HttpClient.HttpClient, client))) @@ -63,7 +66,7 @@ it.live("orgsByAccount groups orgs per account", () => url: "https://one.example.com", accessToken: AccessToken.make("at_1"), refreshToken: RefreshToken.make("rt_1"), - expiry: Date.now() + 10 * 60_000, + expiry: Date.now() + outsideEagerRefreshWindow, orgID: Option.none(), }), ) @@ -75,7 +78,7 @@ it.live("orgsByAccount groups orgs per account", () => url: "https://two.example.com", accessToken: AccessToken.make("at_2"), refreshToken: RefreshToken.make("rt_2"), - expiry: Date.now() + 10 * 60_000, + expiry: Date.now() + outsideEagerRefreshWindow, orgID: Option.none(), }), ) @@ -159,7 +162,7 @@ it.live("token refreshes before expiry when inside the eager refresh window", () url: "https://one.example.com", accessToken: AccessToken.make("at_old"), refreshToken: RefreshToken.make("rt_old"), - expiry: Date.now() + 60_000, + expiry: Date.now() + insideEagerRefreshWindow, orgID: Option.none(), }), ) @@ -267,7 +270,7 @@ it.live("config sends the selected org header", () => url: "https://one.example.com", accessToken: AccessToken.make("at_1"), refreshToken: RefreshToken.make("rt_1"), - expiry: Date.now() + 10 * 60_000, + expiry: Date.now() + outsideEagerRefreshWindow, orgID: Option.none(), }), ) From 336d28f112212efb970543b9995a853753e7d988 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 1 Apr 2026 23:21:07 -0400 Subject: [PATCH 04/19] fix(cli): restore colored help logo (#20592) --- packages/opencode/src/cli/ui.ts | 59 ++++++++++++++++++++++++++++++--- packages/opencode/src/index.ts | 28 +++++++++++++--- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts index c592d4206ce..b24a2a2f442 100644 --- a/packages/opencode/src/cli/ui.ts +++ b/packages/opencode/src/cli/ui.ts @@ -1,6 +1,7 @@ import z from "zod" import { EOL } from "os" import { NamedError } from "@opencode-ai/util/error" +import { logo as glyphs } from "./logo" export namespace UI { const wordmark = [ @@ -47,12 +48,60 @@ export namespace UI { } export function logo(pad?: string) { - const result = [] - for (const row of wordmark) { - if (pad) result.push(pad) - result.push(row) - result.push(EOL) + if (!process.stdout.isTTY && !process.stderr.isTTY) { + const result = [] + for (const row of wordmark) { + if (pad) result.push(pad) + result.push(row) + result.push(EOL) + } + return result.join("").trimEnd() } + + const result: string[] = [] + const reset = "\x1b[0m" + const left = { + fg: "\x1b[90m", + shadow: "\x1b[38;5;235m", + bg: "\x1b[48;5;235m", + } + const right = { + fg: reset, + shadow: "\x1b[38;5;238m", + bg: "\x1b[48;5;238m", + } + const gap = " " + const draw = (line: string, fg: string, shadow: string, bg: string) => { + const parts: string[] = [] + for (const char of line) { + if (char === "_") { + parts.push(bg, " ", reset) + continue + } + if (char === "^") { + parts.push(fg, bg, "▀", reset) + continue + } + if (char === "~") { + parts.push(shadow, "▀", reset) + continue + } + if (char === " ") { + parts.push(" ") + continue + } + parts.push(fg, char, reset) + } + return parts.join("") + } + glyphs.left.forEach((row, index) => { + if (pad) result.push(pad) + result.push(draw(row, left.fg, left.shadow, left.bg)) + result.push(gap) + const other = glyphs.right[index] ?? "" + result.push(draw(other, right.fg, right.shadow, right.bg)) + result.push(EOL) + }) return result.join("").trimEnd() } diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 2da35ace1dd..bb14e0588af 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -48,7 +48,19 @@ process.on("uncaughtException", (e) => { }) }) -const cli = yargs(hideBin(process.argv)) +const args = hideBin(process.argv) + +function show(out: string) { + const text = out.trimStart() + if (!text.startsWith("opencode ")) { + process.stderr.write(UI.logo() + EOL + EOL) + process.stderr.write(text) + return + } + process.stderr.write(out) +} + +const cli = yargs(args) .parserConfiguration({ "populate--": true }) .scriptName("opencode") .wrap(100) @@ -130,7 +142,7 @@ const cli = yargs(hideBin(process.argv)) process.stderr.write("Database migration complete." + EOL) } }) - .usage("\n" + UI.logo()) + .usage("") .completion("completion", "generate shell completion script") .command(AcpCommand) .command(McpCommand) @@ -162,7 +174,7 @@ const cli = yargs(hideBin(process.argv)) msg?.startsWith("Invalid values:") ) { if (err) throw err - cli.showHelp("log") + cli.showHelp(show) } if (err) throw err process.exit(1) @@ -170,7 +182,15 @@ const cli = yargs(hideBin(process.argv)) .strict() try { - await cli.parse() + if (args.includes("-h") || args.includes("--help")) { + await cli.parse(args, (err: Error | undefined, _argv: unknown, out: string) => { + if (err) throw err + if (!out) return + show(out) + }) + } else { + await cli.parse() + } } catch (e) { let data: Record = {} if (e instanceof NamedError) { From d66e6dc25f16bf08c99c80bad4b8c5bac4d3a81f Mon Sep 17 00:00:00 2001 From: dpuyosa Date: Thu, 2 Apr 2026 06:05:49 +0200 Subject: [PATCH 05/19] feat(opencode): Add Venice AI package as dependency (#20570) --- bun.lock | 3 +++ packages/opencode/package.json | 1 + packages/opencode/src/provider/provider.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/bun.lock b/bun.lock index 2b37d21ccd5..40099dddd3a 100644 --- a/bun.lock +++ b/bun.lock @@ -382,6 +382,7 @@ "tree-sitter-powershell": "0.25.10", "turndown": "7.2.0", "ulid": "catalog:", + "venice-ai-sdk-provider": "2.0.1", "vscode-jsonrpc": "8.2.1", "web-tree-sitter": "0.25.10", "which": "6.0.1", @@ -4755,6 +4756,8 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.37", "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.21" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-6SxA8a4MoA6Q/c+D3q7My0Hfog76enN3n0MXhwosM+tso66rXBEGeBRD/0lravRDVzL2Q1w5QJPc86rAVJtfXg=="], + "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 5893dcaa51f..a59d742183f 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -147,6 +147,7 @@ "tree-sitter-powershell": "0.25.10", "turndown": "7.2.0", "ulid": "catalog:", + "venice-ai-sdk-provider": "2.0.1", "vscode-jsonrpc": "8.2.1", "web-tree-sitter": "0.25.10", "which": "6.0.1", diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 45fbeb7e025..b141bff7b3a 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -44,6 +44,7 @@ import { createGateway } from "@ai-sdk/gateway" import { createTogetherAI } from "@ai-sdk/togetherai" import { createPerplexity } from "@ai-sdk/perplexity" import { createVercel } from "@ai-sdk/vercel" +import { createVenice } from "venice-ai-sdk-provider" import { createGitLab, VERSION as GITLAB_PROVIDER_VERSION, @@ -139,6 +140,7 @@ export namespace Provider { "@ai-sdk/vercel": createVercel, "gitlab-ai-provider": createGitLab, "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, + "venice-ai-sdk-provider": createVenice } type CustomModelLoader = (sdk: any, modelID: string, options?: Record) => Promise From ebaa99aba2f98020d2cfb9f0a969c623d0e74f98 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 2 Apr 2026 04:06:47 +0000 Subject: [PATCH 06/19] chore: generate --- packages/opencode/src/provider/provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index b141bff7b3a..441f84b907c 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -140,7 +140,7 @@ export namespace Provider { "@ai-sdk/vercel": createVercel, "gitlab-ai-provider": createGitLab, "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, - "venice-ai-sdk-provider": createVenice + "venice-ai-sdk-provider": createVenice, } type CustomModelLoader = (sdk: any, modelID: string, options?: Record) => Promise From 811c7e2494d307d9fcff8d70262c9de5e1d0eeb2 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 2 Apr 2026 00:25:21 -0400 Subject: [PATCH 07/19] cli: update usage exceeded error --- packages/opencode/src/session/retry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 8ba48375bcf..ec1116da0bb 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -54,7 +54,7 @@ export namespace SessionRetry { if (MessageV2.APIError.isInstance(error)) { if (!error.data.isRetryable) return undefined if (error.data.responseBody?.includes("FreeUsageLimitError")) - return `Free usage exceeded, add credits https://opencode.ai/zen` + return `Free usage exceeded, subscribe to Go https://opencode.ai/go` return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message } From a6f524ca08548e39de15b0d61ae9ef3db1e56b0e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 2 Apr 2026 04:47:27 +0000 Subject: [PATCH 08/19] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index b9e7bb9db27..3c38eb881fa 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-bjfe8/aD0hvUQQEfaNdmKV/Y3dzpf8oz1OUJdgf61WI=", - "aarch64-linux": "sha256-iU9v+ekSCB/qTUG+pOOpSMhPh+0hWnWU5jzDNllEkxU=", - "aarch64-darwin": "sha256-SgNydQLeAjbX0J49f2VKcgKg2Y30pK826R2qQJBMWE4=", - "x86_64-darwin": "sha256-/rzwNuI9x55qi0UcU7QvPUTupErmkt62T09g1omXkQk=" + "x86_64-linux": "sha256-aUDDy/Y6qBRWbCo5oFsocii1DWSsw2qRheoKEMw5E0w=", + "aarch64-linux": "sha256-mVPve3nyrlpWkaERQfvzue8BpWgIXsIg4KnXHchYg2w=", + "aarch64-darwin": "sha256-54W7ZHqH67zib+SarZyQhP2hf18hv8fKsI+QPiUGBwY=", + "x86_64-darwin": "sha256-n1yP4kEl9+a6orKUVdeLrmSxau2+pT9dTneT8ws5Fks=" } } From 0f488996b30780f6fe9ecd08d2b3701024b5eea3 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 2 Apr 2026 14:05:36 +0800 Subject: [PATCH 09/19] fix(node): set OPENCODE_CHANNEL during build (#20616) --- packages/opencode/script/build-node.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/script/build-node.ts b/packages/opencode/script/build-node.ts index 17bc86307af..fc515a67a6b 100644 --- a/packages/opencode/script/build-node.ts +++ b/packages/opencode/script/build-node.ts @@ -1,5 +1,6 @@ #!/usr/bin/env bun +import { Script } from "@opencode-ai/script" import fs from "fs" import path from "path" import { fileURLToPath } from "url" @@ -48,6 +49,7 @@ await Bun.build({ external: ["jsonc-parser"], define: { OPENCODE_MIGRATIONS: JSON.stringify(migrations), + OPENCODE_CHANNEL: `'${Script.channel}'`, }, }) From db938913736600ce3ad68d89a9a3532c4cd517f5 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 2 Apr 2026 03:15:33 -0400 Subject: [PATCH 10/19] zen: friendly trial ended message --- packages/console/app/src/i18n/ar.ts | 2 ++ packages/console/app/src/i18n/br.ts | 2 ++ packages/console/app/src/i18n/da.ts | 2 ++ packages/console/app/src/i18n/de.ts | 2 ++ packages/console/app/src/i18n/en.ts | 2 ++ packages/console/app/src/i18n/es.ts | 2 ++ packages/console/app/src/i18n/fr.ts | 2 ++ packages/console/app/src/i18n/it.ts | 2 ++ packages/console/app/src/i18n/ja.ts | 2 ++ packages/console/app/src/i18n/ko.ts | 2 ++ packages/console/app/src/i18n/no.ts | 2 ++ packages/console/app/src/i18n/pl.ts | 2 ++ packages/console/app/src/i18n/ru.ts | 2 ++ packages/console/app/src/i18n/th.ts | 2 ++ packages/console/app/src/i18n/tr.ts | 2 ++ packages/console/app/src/i18n/zh.ts | 1 + packages/console/app/src/i18n/zht.ts | 1 + packages/console/app/src/routes/zen/util/handler.ts | 8 ++++++++ packages/console/core/src/model.ts | 1 + 19 files changed, 41 insertions(+) diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 393396c36a8..8896df1c8b6 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -363,6 +363,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "لقد وصلت إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{membersUrl}}", "zen.api.error.modelDisabled": "النموذج معطل", + "zen.api.error.trialEnded": + "انتهى العرض المجاني لـ {{model}}. يمكنك مواصلة استخدام النموذج بالاشتراك في OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | الوصول إلى أفضل نماذج البرمجة في العالم", "black.meta.description": "احصل على وصول إلى Claude، GPT، Gemini والمزيد مع خطط اشتراك OpenCode Black.", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index 764434264f6..0259b5f8845 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -371,6 +371,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Você atingiu seu limite de gastos mensais de ${{amount}}. Gerencie seus limites aqui: {{membersUrl}}", "zen.api.error.modelDisabled": "O modelo está desabilitado", + "zen.api.error.trialEnded": + "A promoção gratuita do {{model}} terminou. Você pode continuar usando o modelo assinando o OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Acesse os melhores modelos de codificação do mundo", "black.meta.description": "Tenha acesso ao Claude, GPT, Gemini e mais com os planos de assinatura OpenCode Black.", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index 3f9d9deaced..5a376cb5df7 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -368,6 +368,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Du har nået din månedlige forbrugsgrænse på ${{amount}}. Administrer dine grænser her: {{membersUrl}}", "zen.api.error.modelDisabled": "Modellen er deaktiveret", + "zen.api.error.trialEnded": + "Den gratis kampagne for {{model}} er afsluttet. Du kan fortsætte med at bruge modellen ved at abonnere på OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Få adgang til verdens bedste kodningsmodeller", "black.meta.description": "Få adgang til Claude, GPT, Gemini og mere med OpenCode Black-abonnementer.", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 535bafe514e..d6e8533921b 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -371,6 +371,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Du hast dein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{membersUrl}}", "zen.api.error.modelDisabled": "Modell ist deaktiviert", + "zen.api.error.trialEnded": + "Die kostenlose Aktion für {{model}} ist beendet. Du kannst das Modell weiterhin nutzen, indem du OpenCode Go abonnierst - {{link}}", "black.meta.title": "OpenCode Black | Zugriff auf die weltweit besten Coding-Modelle", "black.meta.description": "Erhalte Zugriff auf Claude, GPT, Gemini und mehr mit OpenCode Black Abos.", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index c68711dd254..49339a3f526 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -364,6 +364,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "You have reached your monthly spending limit of ${{amount}}. Manage your limits here: {{membersUrl}}", "zen.api.error.modelDisabled": "Model is disabled", + "zen.api.error.trialEnded": + "Free promotion has ended for {{model}}. You can continue using the model by subscribing to OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Access all the world's best coding models", "black.meta.description": "Get access to Claude, GPT, Gemini and more with OpenCode Black subscription plans.", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index d56099d7a60..632a29a1d26 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -371,6 +371,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Has alcanzado tu límite de gasto mensual de ${{amount}}. Gestiona tus límites aquí: {{membersUrl}}", "zen.api.error.modelDisabled": "El modelo está deshabilitado", + "zen.api.error.trialEnded": + "La promoción gratuita de {{model}} ha finalizado. Puedes seguir usando el modelo suscribiéndote a OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Accede a los mejores modelos de codificación del mundo", "black.meta.description": "Obtén acceso a Claude, GPT, Gemini y más con los planes de suscripción de OpenCode Black.", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index ad1d377cc0f..f657c6164fe 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -372,6 +372,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Vous avez atteint votre limite de dépense mensuelle de {{amount}} $. Gérez vos limites ici : {{membersUrl}}", "zen.api.error.modelDisabled": "Le modèle est désactivé", + "zen.api.error.trialEnded": + "La promotion gratuite de {{model}} est terminée. Vous pouvez continuer à utiliser le modèle en vous abonnant à OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Accédez aux meilleurs modèles de code au monde", "black.meta.description": "Accédez à Claude, GPT, Gemini et plus avec les forfaits d'abonnement OpenCode Black.", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index 0b6b6b6cfa7..c3c73b2835c 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -367,6 +367,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Hai raggiunto il tuo limite di spesa mensile di ${{amount}}. Gestisci i tuoi limiti qui: {{membersUrl}}", "zen.api.error.modelDisabled": "Il modello è disabilitato", + "zen.api.error.trialEnded": + "La promozione gratuita di {{model}} è terminata. Puoi continuare a usare il modello abbonandoti a OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Accedi ai migliori modelli di coding al mondo", "black.meta.description": diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index 3ef298ee312..f645637f078 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -369,6 +369,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{membersUrl}}", "zen.api.error.modelDisabled": "モデルが無効です", + "zen.api.error.trialEnded": + "{{model}} の無料プロモーションは終了しました。OpenCode Go を購読するとモデルを引き続き使用できます - {{link}}", "black.meta.title": "OpenCode Black | 世界最高峰のコーディングモデルすべてにアクセス", "black.meta.description": "OpenCode Black サブスクリプションプランで、Claude、GPT、Gemini などにアクセス。", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index 526791652cd..f2a6147130f 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -363,6 +363,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{membersUrl}}", "zen.api.error.modelDisabled": "모델이 비활성화되었습니다", + "zen.api.error.trialEnded": + "{{model}}의 무료 프로모션이 종료되었습니다. OpenCode Go를 구독하면 모델을 계속 사용할 수 있습니다 - {{link}}", "black.meta.title": "OpenCode Black | 세계 최고의 코딩 모델에 액세스하세요", "black.meta.description": "OpenCode Black 구독 플랜으로 Claude, GPT, Gemini 등에 액세스하세요.", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index 3e405506b3e..95c55c1e994 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -368,6 +368,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Du har nådd din månedlige utgiftsgrense på ${{amount}}. Administrer grensene dine her: {{membersUrl}}", "zen.api.error.modelDisabled": "Modellen er deaktivert", + "zen.api.error.trialEnded": + "Den gratis kampanjen for {{model}} er avsluttet. Du kan fortsette å bruke modellen ved å abonnere på OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Få tilgang til verdens beste kodemodeller", "black.meta.description": "Få tilgang til Claude, GPT, Gemini og mer med OpenCode Black-abonnementer.", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index 513d8f9cc9d..c119fad5b39 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -369,6 +369,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Osiągnąłeś swój miesięczny limit wydatków w wysokości ${{amount}}. Zarządzaj swoimi limitami tutaj: {{membersUrl}}", "zen.api.error.modelDisabled": "Model jest wyłączony", + "zen.api.error.trialEnded": + "Bezpłatna promocja {{model}} dobiegła końca. Możesz dalej korzystać z modelu, subskrybując OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Dostęp do najlepszych na świecie modeli kodujących", "black.meta.description": "Uzyskaj dostęp do Claude, GPT, Gemini i innych dzięki planom subskrypcji OpenCode Black.", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index ed90e615847..4b9401af9e4 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -373,6 +373,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Вы достигли ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{membersUrl}}", "zen.api.error.modelDisabled": "Модель отключена", + "zen.api.error.trialEnded": + "Бесплатная акция для {{model}} завершена. Вы можете продолжить использование модели, подписавшись на OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Доступ к лучшим моделям для кодинга в мире", "black.meta.description": "Получите доступ к Claude, GPT, Gemini и другим моделям с подпиской OpenCode Black.", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index 3eb7e631233..8ba75548a6c 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -365,6 +365,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "คุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{membersUrl}}", "zen.api.error.modelDisabled": "โมเดลถูกปิดใช้งาน", + "zen.api.error.trialEnded": + "โปรโมชันฟรีสำหรับ {{model}} สิ้นสุดแล้ว คุณสามารถใช้โมเดลต่อได้โดยสมัครสมาชิก OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | เข้าถึงโมเดลเขียนโค้ดที่ดีที่สุดในโลก", "black.meta.description": "เข้าถึง Claude, GPT, Gemini และอื่นๆ ด้วยแผนสมาชิก OpenCode Black", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index a663e651190..ce3ec7e1d66 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -372,6 +372,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Aylık ${{amount}} harcama limitinize ulaştınız. Limitlerinizi buradan yönetin: {{membersUrl}}", "zen.api.error.modelDisabled": "Model devre dışı", + "zen.api.error.trialEnded": + "{{model}} için ücretsiz promosyon sona erdi. OpenCode Go'ya abone olarak modeli kullanmaya devam edebilirsiniz - {{link}}", "black.meta.title": "OpenCode Black | Dünyanın en iyi kodlama modellerine erişin", "black.meta.description": "OpenCode Black abonelik planlarıyla Claude, GPT, Gemini ve daha fazlasına erişin.", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index cbf4932ef2d..0803ffd132d 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -349,6 +349,7 @@ export const dict = { "您的工作区已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{billingUrl}}", "zen.api.error.userMonthlyLimitReached": "您已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{membersUrl}}", "zen.api.error.modelDisabled": "模型已禁用", + "zen.api.error.trialEnded": "{{model}} 的限免活动已结束。您可以订阅 OpenCode Go 继续使用该模型 - {{link}}", "black.meta.title": "OpenCode Black | 访问全球顶尖编程模型", "black.meta.description": "通过 OpenCode Black 订阅计划使用 Claude, GPT, Gemini 等模型。", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index 0670c11936d..66e242eb767 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -349,6 +349,7 @@ export const dict = { "你的工作區已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{billingUrl}}", "zen.api.error.userMonthlyLimitReached": "你已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{membersUrl}}", "zen.api.error.modelDisabled": "模型已停用", + "zen.api.error.trialEnded": "{{model}} 的限免活动已結束。您可以訂閱 OpenCode Go 繼續使用該模型 - {{link}}", "black.meta.title": "OpenCode Black | 存取全球最佳編碼模型", "black.meta.description": "透過 OpenCode Black 訂閱方案存取 Claude、GPT、Gemini 等模型。", diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 53bd0e6012b..db5977bc16f 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -404,6 +404,14 @@ export async function handler( }), ) + if (modelData.trialEnded) + throw new ModelError( + `${t("zen.api.error.trialEnded", { + model: modelData.name, + link: "https://opencode.ai/go", + })}`, + ) + logger.metric({ model: modelId }) return { id: modelId, ...modelData } diff --git a/packages/console/core/src/model.ts b/packages/console/core/src/model.ts index 191fdf1b7e4..3b243943163 100644 --- a/packages/console/core/src/model.ts +++ b/packages/console/core/src/model.ts @@ -27,6 +27,7 @@ export namespace ZenData { byokProvider: z.enum(["openai", "anthropic", "google"]).optional(), stickyProvider: z.enum(["strict", "prefer"]).optional(), trialProviders: z.array(z.string()).optional(), + trialEnded: z.boolean().optional(), fallbackProvider: z.string().optional(), rateLimit: z.number().optional(), providers: z.array( From d540d363a76909c9c1b1d4e4113a1b8fea62c5a8 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 2 Apr 2026 14:44:05 +0530 Subject: [PATCH 11/19] refactor: simplify solid reactivity across app and web (#20497) --- packages/app/src/pages/session.tsx | 5 +- .../components/line-comment-annotations.tsx | 5 -- packages/ui/src/components/line-comment.tsx | 19 +++----- packages/ui/src/components/message-part.tsx | 31 ++++++++---- packages/ui/src/components/session-turn.tsx | 5 +- packages/web/src/components/Share.tsx | 14 ++---- packages/web/src/components/share/common.tsx | 11 +++-- .../web/src/components/share/content-diff.tsx | 47 +++++++++++-------- 8 files changed, 70 insertions(+), 67 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 18bae6e2d0d..e51161590ea 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -329,10 +329,9 @@ export default function Page() { const { params, sessionKey, tabs, view } = useSessionLayout() createEffect(() => { - if (!untrack(() => prompt.ready())) return - prompt.ready() + if (!prompt.ready()) return untrack(() => { - if (params.id || !prompt.ready()) return + if (params.id) return const text = searchParams.prompt if (!text) return prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) diff --git a/packages/ui/src/components/line-comment-annotations.tsx b/packages/ui/src/components/line-comment-annotations.tsx index 80018d3dda0..f0286a36a92 100644 --- a/packages/ui/src/components/line-comment-annotations.tsx +++ b/packages/ui/src/components/line-comment-annotations.tsx @@ -294,11 +294,6 @@ export function createLineCommentState(props: LineCommentStateProps) { cancelDraft() } - createEffect(() => { - props.commenting() - setDraft("") - }) - return { draft, setDraft, diff --git a/packages/ui/src/components/line-comment.tsx b/packages/ui/src/components/line-comment.tsx index f0e29a485d5..26e763bb3ec 100644 --- a/packages/ui/src/components/line-comment.tsx +++ b/packages/ui/src/components/line-comment.tsx @@ -1,6 +1,6 @@ import { useFilteredList } from "@opencode-ai/ui/hooks" import { getDirectory, getFilename } from "@opencode-ai/util/path" -import { createEffect, createSignal, For, onMount, Show, splitProps, type JSX } from "solid-js" +import { createSignal, For, onMount, Show, splitProps, type JSX } from "solid-js" import { Button } from "./button" import { FileIcon } from "./file-icon" import { Icon } from "./icon" @@ -210,7 +210,6 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { const refs = { textarea: undefined as HTMLTextAreaElement | undefined, } - const [text, setText] = createSignal(split.value) const [open, setOpen] = createSignal(false) function selectMention(item: { path: string } | undefined) { @@ -220,10 +219,9 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { const query = currentMention() if (!textarea || !query) return - const value = `${text().slice(0, query.start)}@${item.path} ${text().slice(query.end)}` + const value = `${textarea.value.slice(0, query.start)}@${item.path} ${textarea.value.slice(query.end)}` const cursor = query.start + item.path.length + 2 - setText(value) split.onInput(value) closeMention() @@ -257,10 +255,6 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { fn() } - createEffect(() => { - setText(split.value) - }) - const closeMention = () => { setOpen(false) mention.clear() @@ -302,7 +296,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { } const submit = () => { - const value = text().trim() + const value = split.value.trim() if (!value) return split.onSubmit(value) } @@ -322,10 +316,9 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { data-slot="line-comment-textarea" rows={split.rows ?? 3} placeholder={split.placeholder ?? i18n.t("ui.lineComment.placeholder")} - value={text()} + value={split.value} on:input={(e) => { const value = (e.currentTarget as HTMLTextAreaElement).value - setText(value) split.onInput(value) syncMention() }} @@ -422,7 +415,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { type="button" data-slot="line-comment-action" data-variant="primary" - disabled={text().trim().length === 0} + disabled={split.value.trim().length === 0} on:mousedown={hold as any} on:click={click(submit) as any} > @@ -434,7 +427,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { - diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 1555a09a079..03477e5a7f2 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -230,6 +230,19 @@ function createPacedValue(getValue: () => string, live?: () => boolean) { return value } +function PacedMarkdown(props: { text: string; cacheKey: string; streaming: boolean }) { + const value = createPacedValue( + () => props.text, + () => props.streaming, + ) + + return ( + + + + ) +} + function relativizeProjectPath(path: string, directory?: string) { if (!path) return "" if (!directory) return path @@ -1373,8 +1386,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { const streaming = createMemo( () => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number", ) - const displayText = () => (part().text ?? "").trim() - const throttledText = createPacedValue(displayText, streaming) + const text = () => (part().text ?? "").trim() const isLastTextPart = createMemo(() => { const last = (data.store.part?.[props.message.id] ?? []) .filter((item): item is TextPart => item?.type === "text" && !!item.text?.trim()) @@ -1390,7 +1402,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { const [copied, setCopied] = createSignal(false) const handleCopy = async () => { - const content = displayText() + const content = text() if (!content) return await navigator.clipboard.writeText(content) setCopied(true) @@ -1398,10 +1410,12 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { } return ( - +
- + }> + +
@@ -1437,12 +1451,13 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) { () => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number", ) const text = () => part().text.trim() - const throttledText = createPacedValue(text, streaming) return ( - +
- + }> + +
) diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index ed4c0e91491..fe029485a1b 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -343,14 +343,12 @@ export function SessionTurn( }) const assistantDerived = createMemo(() => { let visible = 0 - let tail: "text" | "other" | undefined let reason: string | undefined const show = showReasoningSummaries() for (const message of assistantMessages()) { for (const part of list(data.store.part?.[message.id], emptyParts)) { if (partState(part, show) === "visible") { visible++ - tail = part.type === "text" ? "text" : "other" } if (part.type === "reasoning" && part.text) { const h = heading(part.text) @@ -358,10 +356,9 @@ export function SessionTurn( } } } - return { visible, tail, reason } + return { visible, reason } }) const assistantVisible = createMemo(() => assistantDerived().visible) - const assistantTailVisible = createMemo(() => assistantDerived().tail) const reasoningHeading = createMemo(() => assistantDerived().reason) const showThinking = createMemo(() => { if (!working() || !!error()) return false diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx index de12baede06..3ee86c270db 100644 --- a/packages/web/src/components/Share.tsx +++ b/packages/web/src/components/Share.tsx @@ -366,21 +366,13 @@ export default function Share(props: { {(part, partIndex) => { - const last = createMemo( - () => - data().messages.length === msgIndex() + 1 && - filteredParts().length === partIndex() + 1, - ) + const last = () => + data().messages.length === msgIndex() + 1 && filteredParts().length === partIndex() + 1 onMount(() => { const hash = window.location.hash.slice(1) // Wait till all parts are loaded - if ( - hash !== "" && - !hasScrolledToAnchor && - filteredParts().length === partIndex() + 1 && - data().messages.length === msgIndex() + 1 - ) { + if (hash !== "" && !hasScrolledToAnchor && last()) { hasScrolledToAnchor = true scrollToAnchor(hash) } diff --git a/packages/web/src/components/share/common.tsx b/packages/web/src/components/share/common.tsx index 7ca4daa6ac8..aebc9553773 100644 --- a/packages/web/src/components/share/common.tsx +++ b/packages/web/src/components/share/common.tsx @@ -83,12 +83,15 @@ export function createOverflow() { return overflow() }, ref(el: HTMLElement) { + const sync = () => { + setOverflow(el.scrollHeight > el.clientHeight + 1) + } + const ro = new ResizeObserver(() => { - if (el.scrollHeight > el.clientHeight + 1) { - setOverflow(true) - } - return + sync() }) + + sync() ro.observe(el) onCleanup(() => { diff --git a/packages/web/src/components/share/content-diff.tsx b/packages/web/src/components/share/content-diff.tsx index 9ccd554d048..c8dd35b38f1 100644 --- a/packages/web/src/components/share/content-diff.tsx +++ b/packages/web/src/components/share/content-diff.tsx @@ -1,5 +1,5 @@ import { parsePatch } from "diff" -import { createMemo } from "solid-js" +import { createMemo, For } from "solid-js" import { ContentCode } from "./content-code" import styles from "./content-diff.module.css" @@ -160,28 +160,37 @@ export function ContentDiff(props: Props) { return (
- {rows().map((r) => ( -
-
- + + {(row) => ( +
+
+ +
+
+ +
-
- -
-
- ))} + )} +
- {mobileRows().map((block) => ( -
- {block.lines.map((line) => ( -
- -
- ))} -
- ))} + + {(block) => ( +
+ + {(line) => ( +
+ +
+ )} +
+
+ )} +
) From 327f62526a7f60c1c67ae017d1b105466bb730e9 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 2 Apr 2026 17:24:10 +0800 Subject: [PATCH 12/19] use solid-primitives/resize-observer across web code (#20613) --- bun.lock | 12 ++++++++++-- packages/app/package.json | 3 ++- .../app/src/components/server/server-row.tsx | 9 +++------ .../composer/session-composer-region.tsx | 9 +++------ .../session/composer/session-question-dock.tsx | 11 +++++------ .../session/composer/session-todo-dock.tsx | 5 ++--- packages/ui/src/components/scroll-view.tsx | 17 +++-------------- .../components/text-strikethrough.stories.tsx | 11 +++-------- .../ui/src/components/text-strikethrough.tsx | 12 +++--------- packages/ui/src/pierre/file-find.ts | 5 ++--- packages/web/package.json | 1 + packages/web/src/components/share/common.tsx | 13 ++++--------- 12 files changed, 41 insertions(+), 67 deletions(-) diff --git a/bun.lock b/bun.lock index 40099dddd3a..b4bff1e21e0 100644 --- a/bun.lock +++ b/bun.lock @@ -36,9 +36,10 @@ "@solid-primitives/active-element": "2.1.3", "@solid-primitives/audio": "1.4.2", "@solid-primitives/event-bus": "1.1.2", + "@solid-primitives/event-listener": "2.4.5", "@solid-primitives/i18n": "2.2.1", "@solid-primitives/media": "2.3.3", - "@solid-primitives/resize-observer": "2.1.3", + "@solid-primitives/resize-observer": "2.1.5", "@solid-primitives/scroll": "2.1.3", "@solid-primitives/storage": "catalog:", "@solid-primitives/timer": "1.4.4", @@ -572,6 +573,7 @@ "@astrojs/starlight": "0.34.3", "@fontsource/ibm-plex-mono": "5.2.5", "@shikijs/transformers": "3.20.0", + "@solid-primitives/resize-observer": "2.1.5", "@types/luxon": "catalog:", "ai": "catalog:", "astro": "5.7.13", @@ -1934,7 +1936,7 @@ "@solid-primitives/refs": ["@solid-primitives/refs@1.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA=="], - "@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + "@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/static-store": "^0.1.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AiyTknKcNBaKHbcSMuxtSNM8FjIuiSuFyFghdD0TcCMU9hKi9EmsC5pjfjDwxE+5EueB1a+T/34PLRI5vbBbKw=="], "@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="], @@ -5212,6 +5214,8 @@ "@jsx-email/doiuse-email/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="], + "@kobalte/core/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], @@ -5304,6 +5308,8 @@ "@opencode-ai/desktop-electron/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], + "@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], "@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], @@ -5366,6 +5372,8 @@ "@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@solid-primitives/bounds/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + "@solidjs/start/path-to-regexp": ["path-to-regexp@8.4.1", "", {}, "sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw=="], "@solidjs/start/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="], diff --git a/packages/app/package.json b/packages/app/package.json index 670bec60e14..d179e4a5241 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -46,9 +46,10 @@ "@solid-primitives/active-element": "2.1.3", "@solid-primitives/audio": "1.4.2", "@solid-primitives/event-bus": "1.1.2", + "@solid-primitives/event-listener": "2.4.5", "@solid-primitives/i18n": "2.2.1", "@solid-primitives/media": "2.3.3", - "@solid-primitives/resize-observer": "2.1.3", + "@solid-primitives/resize-observer": "2.1.5", "@solid-primitives/scroll": "2.1.3", "@solid-primitives/storage": "catalog:", "@solid-primitives/timer": "1.4.4", diff --git a/packages/app/src/components/server/server-row.tsx b/packages/app/src/components/server/server-row.tsx index 63a40bac241..d4f68d6306d 100644 --- a/packages/app/src/components/server/server-row.tsx +++ b/packages/app/src/components/server/server-row.tsx @@ -1,11 +1,11 @@ import { Tooltip } from "@opencode-ai/ui/tooltip" +import { createResizeObserver } from "@solid-primitives/resize-observer" import { children, createEffect, createMemo, createSignal, type JSXElement, - onCleanup, onMount, type ParentProps, Show, @@ -46,12 +46,9 @@ export function ServerRow(props: ServerRowProps) { }) onMount(() => { - check() if (typeof ResizeObserver !== "function") return - const observer = new ResizeObserver(check) - if (nameRef) observer.observe(nameRef) - if (versionRef) observer.observe(versionRef) - onCleanup(() => observer.disconnect()) + createResizeObserver([nameRef, versionRef], check) + check() }) const tooltipValue = () => ( diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx index a5263cd743e..372adef96af 100644 --- a/packages/app/src/pages/session/composer/session-composer-region.tsx +++ b/packages/app/src/pages/session/composer/session-composer-region.tsx @@ -13,6 +13,7 @@ import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock" import type { SessionComposerState } from "@/pages/session/composer/session-composer-state" import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock" import type { FollowupDraft } from "@/components/prompt-input/submit" +import { createResizeObserver } from "@solid-primitives/resize-observer" export function SessionComposerRegion(props: { state: SessionComposerState @@ -115,13 +116,9 @@ export function SessionComposerRegion(props: { createEffect(() => { const el = store.body if (!el) return - const update = () => { - setStore("height", el.getBoundingClientRect().height) - } + const update = () => setStore("height", el.getBoundingClientRect().height) + createResizeObserver(store.body, update) update() - const observer = new ResizeObserver(update) - observer.observe(el) - onCleanup(() => observer.disconnect()) }) return ( diff --git a/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx index 38974b2465c..35690030c91 100644 --- a/packages/app/src/pages/session/composer/session-question-dock.tsx +++ b/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -8,6 +8,8 @@ import { showToast } from "@opencode-ai/ui/toast" import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2" import { useLanguage } from "@/context/language" import { useSDK } from "@/context/sdk" +import { makeEventListener } from "@solid-primitives/event-listener" +import { createResizeObserver } from "@solid-primitives/resize-observer" const cache = new Map() @@ -172,17 +174,14 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit } update() - window.addEventListener("resize", update) + + makeEventListener(window, "resize", update) const dock = root?.closest('[data-component="session-prompt-dock"]') const scroller = document.querySelector(".scroll-view__viewport") - const observer = new ResizeObserver(update) - if (dock instanceof HTMLElement) observer.observe(dock) - if (scroller instanceof HTMLElement) observer.observe(scroller) + createResizeObserver([dock, scroller], update) onCleanup(() => { - window.removeEventListener("resize", update) - observer.disconnect() if (raf !== undefined) cancelAnimationFrame(raf) }) diff --git a/packages/app/src/pages/session/composer/session-todo-dock.tsx b/packages/app/src/pages/session/composer/session-todo-dock.tsx index c16ac839932..7928bcc9ca8 100644 --- a/packages/app/src/pages/session/composer/session-todo-dock.tsx +++ b/packages/app/src/pages/session/composer/session-todo-dock.tsx @@ -6,6 +6,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { useSpring } from "@opencode-ai/ui/motion-spring" import { TextReveal } from "@opencode-ai/ui/text-reveal" import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough" +import { createResizeObserver } from "@solid-primitives/resize-observer" import { Index, createEffect, createMemo, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import { composerEnabled, composerProbe } from "@/testing/session-composer" @@ -91,9 +92,7 @@ export function SessionTodoDock(props: { setStore("height", el.getBoundingClientRect().height) } update() - const observer = new ResizeObserver(update) - observer.observe(el) - onCleanup(() => observer.disconnect()) + createResizeObserver(el, update) }) createEffect(() => { diff --git a/packages/ui/src/components/scroll-view.tsx b/packages/ui/src/components/scroll-view.tsx index 2b58300b9f0..3ff00f117d2 100644 --- a/packages/ui/src/components/scroll-view.tsx +++ b/packages/ui/src/components/scroll-view.tsx @@ -1,4 +1,5 @@ -import { onCleanup, onMount, splitProps, type ComponentProps, Show, mergeProps } from "solid-js" +import { onMount, splitProps, type ComponentProps, Show, mergeProps } from "solid-js" +import { createResizeObserver } from "@solid-primitives/resize-observer" import { createStore } from "solid-js/store" import { useI18n } from "../context/i18n" @@ -97,19 +98,7 @@ export function ScrollView(props: ScrollViewProps) { local.viewportRef(viewportRef) } - const observer = new ResizeObserver(() => { - updateThumb() - }) - - observer.observe(viewportRef) - // Also observe the first child if possible to catch content changes - if (viewportRef.firstElementChild) { - observer.observe(viewportRef.firstElementChild) - } - - onCleanup(() => { - observer.disconnect() - }) + createResizeObserver([viewportRef, viewportRef.firstElementChild], updateThumb) updateThumb() }) diff --git a/packages/ui/src/components/text-strikethrough.stories.tsx b/packages/ui/src/components/text-strikethrough.stories.tsx index 5e86413f93f..5ef06dbcf4f 100644 --- a/packages/ui/src/components/text-strikethrough.stories.tsx +++ b/packages/ui/src/components/text-strikethrough.stories.tsx @@ -1,5 +1,6 @@ // @ts-nocheck -import { createEffect, createSignal, onCleanup, onMount } from "solid-js" +import { createSignal, onMount } from "solid-js" +import { createResizeObserver } from "@solid-primitives/resize-observer" import { createStore } from "solid-js/store" import { useSpring } from "./motion-spring" import { TextStrikethrough } from "./text-strikethrough" @@ -144,13 +145,7 @@ function VariantF(props: { active: boolean; text: string }) { } onMount(measure) - createEffect(() => { - const el = containerRef - if (!el) return - const observer = new ResizeObserver(measure) - observer.observe(el) - onCleanup(() => observer.disconnect()) - }) + createResizeObserver(() => containerRef, measure) const clipRight = () => { const cw = containerWidth() diff --git a/packages/ui/src/components/text-strikethrough.tsx b/packages/ui/src/components/text-strikethrough.tsx index aee5e0cbd0e..958befff68a 100644 --- a/packages/ui/src/components/text-strikethrough.tsx +++ b/packages/ui/src/components/text-strikethrough.tsx @@ -1,5 +1,6 @@ import type { JSX } from "solid-js" -import { createEffect, onCleanup, onMount } from "solid-js" +import { onMount } from "solid-js" +import { createResizeObserver } from "@solid-primitives/resize-observer" import { createStore } from "solid-js/store" import { useSpring } from "./motion-spring" @@ -33,14 +34,7 @@ export function TextStrikethrough(props: { } onMount(measure) - - createEffect(() => { - const el = containerRef - if (!el) return - const observer = new ResizeObserver(measure) - observer.observe(el) - onCleanup(() => observer.disconnect()) - }) + createResizeObserver(() => containerRef, measure) // Revealed pixels from left = progress * textWidth const revealedPx = () => { diff --git a/packages/ui/src/pierre/file-find.ts b/packages/ui/src/pierre/file-find.ts index 692ab31670f..841b57edc5a 100644 --- a/packages/ui/src/pierre/file-find.ts +++ b/packages/ui/src/pierre/file-find.ts @@ -1,4 +1,5 @@ import { createEffect, onCleanup, onMount } from "solid-js" +import { createResizeObserver } from "@solid-primitives/resize-observer" import { createStore } from "solid-js/store" export type FindHost = { @@ -429,12 +430,10 @@ export function createFileFind(opts: CreateFileFindOptions) { const wrapper = opts.wrapper() if (!wrapper) return const root = scrollParent(wrapper) ?? wrapper - const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(() => update()) - observer?.observe(root) + createResizeObserver(root, update) onCleanup(() => { window.removeEventListener("resize", update) - observer?.disconnect() }) }) diff --git a/packages/web/package.json b/packages/web/package.json index ef0d8aa6c34..60082740bbe 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -18,6 +18,7 @@ "@astrojs/starlight": "0.34.3", "@fontsource/ibm-plex-mono": "5.2.5", "@shikijs/transformers": "3.20.0", + "@solid-primitives/resize-observer": "2.1.5", "@types/luxon": "catalog:", "ai": "catalog:", "astro": "5.7.13", diff --git a/packages/web/src/components/share/common.tsx b/packages/web/src/components/share/common.tsx index aebc9553773..ad50e425fa4 100644 --- a/packages/web/src/components/share/common.tsx +++ b/packages/web/src/components/share/common.tsx @@ -1,5 +1,6 @@ -import { createContext, createSignal, onCleanup, splitProps, useContext } from "solid-js" +import { createContext, createSignal, splitProps, useContext } from "solid-js" import type { JSX } from "solid-js/jsx-runtime" +import { makeResizeObserver } from "@solid-primitives/resize-observer" import { IconCheckCircle, IconHashtag } from "../icons" export type ShareMessages = { locale: string } & Record @@ -87,16 +88,10 @@ export function createOverflow() { setOverflow(el.scrollHeight > el.clientHeight + 1) } - const ro = new ResizeObserver(() => { - sync() - }) + const obs = makeResizeObserver(sync) + obs.observe(el) sync() - ro.observe(el) - - onCleanup(() => { - ro.disconnect() - }) }, } } From 69d047ae7dd84d4c4de41e09b1ecee88e3fdc3d3 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 2 Apr 2026 17:40:03 +0800 Subject: [PATCH 13/19] cleanup event listeners with solid-primitives/event-listener (#20619) --- bun.lock | 1 + packages/app/src/components/debug-bar.tsx | 4 +- .../components/prompt-input/attachments.ts | 15 +++---- .../app/src/components/settings-keybinds.tsx | 4 +- packages/app/src/context/command.tsx | 7 +--- packages/app/src/context/global-sdk.tsx | 24 +++++------ packages/app/src/context/layout.tsx | 7 ++-- packages/app/src/pages/layout.tsx | 21 ++++------ packages/app/src/pages/session.tsx | 4 +- .../composer/session-composer-state.ts | 4 +- packages/app/src/pages/session/file-tabs.tsx | 40 ++++++++----------- packages/app/src/pages/session/helpers.ts | 12 ++---- packages/app/src/pages/session/review-tab.tsx | 20 ++++------ .../app/src/pages/session/terminal-panel.tsx | 9 ++--- packages/ui/package.json | 1 + packages/ui/src/components/file.tsx | 16 +++----- packages/ui/src/components/list.tsx | 6 +-- packages/ui/src/components/popover.tsx | 24 +++-------- packages/ui/src/context/dialog.tsx | 4 +- packages/ui/src/hooks/create-auto-scroll.tsx | 18 ++------- packages/ui/src/pierre/file-find.ts | 24 +++++------ packages/ui/src/theme/context.tsx | 13 +++--- 22 files changed, 102 insertions(+), 176 deletions(-) diff --git a/bun.lock b/bun.lock index b4bff1e21e0..2d3f815f5d3 100644 --- a/bun.lock +++ b/bun.lock @@ -515,6 +515,7 @@ "@pierre/diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/bounds": "0.1.3", + "@solid-primitives/event-listener": "2.4.5", "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", "@solidjs/meta": "catalog:", diff --git a/packages/app/src/components/debug-bar.tsx b/packages/app/src/components/debug-bar.tsx index f4b7a1bc0ec..11f9f59e4e0 100644 --- a/packages/app/src/components/debug-bar.tsx +++ b/packages/app/src/components/debug-bar.tsx @@ -1,6 +1,7 @@ import { useIsRouting, useLocation } from "@solidjs/router" import { batch, createEffect, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { Tooltip } from "@opencode-ai/ui/tooltip" import { useLanguage } from "@/context/language" @@ -349,13 +350,12 @@ export function DebugBar() { syncHeap() start() - document.addEventListener("visibilitychange", vis) + makeEventListener(document, "visibilitychange", vis) onCleanup(() => { if (one !== 0) cancelAnimationFrame(one) if (two !== 0) cancelAnimationFrame(two) stop() - document.removeEventListener("visibilitychange", vis) for (const ob of obs) ob.disconnect() }) }) diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts index fa9930f6839..f12a4210c08 100644 --- a/packages/app/src/components/prompt-input/attachments.ts +++ b/packages/app/src/components/prompt-input/attachments.ts @@ -1,4 +1,5 @@ -import { onCleanup, onMount } from "solid-js" +import { onMount } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" import { showToast } from "@opencode-ai/ui/toast" import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt" import { useLanguage } from "@/context/language" @@ -181,15 +182,9 @@ export function createPromptAttachments(input: PromptAttachmentsInput) { } onMount(() => { - document.addEventListener("dragover", handleGlobalDragOver) - document.addEventListener("dragleave", handleGlobalDragLeave) - document.addEventListener("drop", handleGlobalDrop) - }) - - onCleanup(() => { - document.removeEventListener("dragover", handleGlobalDragOver) - document.removeEventListener("dragleave", handleGlobalDragLeave) - document.removeEventListener("drop", handleGlobalDrop) + makeEventListener(document, "dragover", handleGlobalDragOver) + makeEventListener(document, "dragleave", handleGlobalDragLeave) + makeEventListener(document, "drop", handleGlobalDrop) }) return { diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index 7e2a48110cb..7d2dfaa6363 100644 --- a/packages/app/src/components/settings-keybinds.tsx +++ b/packages/app/src/components/settings-keybinds.tsx @@ -1,5 +1,6 @@ import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" @@ -250,8 +251,7 @@ function useKeyCapture(input: { input.stop() } - document.addEventListener("keydown", handle, true) - onCleanup(() => document.removeEventListener("keydown", handle, true)) + makeEventListener(document, "keydown", handle, { capture: true }) }) } diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index 65805f40c8c..d2238828c69 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -2,6 +2,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context" import { useDialog } from "@opencode-ai/ui/context/dialog" import { type Accessor, createEffect, createMemo, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" import { dict as en } from "@/i18n/en" @@ -378,11 +379,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex } onMount(() => { - document.addEventListener("keydown", handleKeyDown) - }) - - onCleanup(() => { - document.removeEventListener("keydown", handleKeyDown) + makeEventListener(document, "keydown", handleKeyDown) }) function register(cb: () => CommandOption[]): void diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx index d240f9eeff5..1205a8fa822 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -1,7 +1,8 @@ import type { Event } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" import { createGlobalEmitter } from "@solid-primitives/event-bus" -import { batch, onCleanup } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" +import { batch, onCleanup, onMount } from "solid-js" import z from "zod" import { createSdkForServer } from "@/utils/server" import { useLanguage } from "./language" @@ -206,21 +207,16 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo clearHeartbeat() } - const onVisibility = () => { - if (typeof document === "undefined") return - if (document.visibilityState !== "visible") return - if (!started) return - if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return - attempt?.abort() - } - if (typeof document !== "undefined") { - document.addEventListener("visibilitychange", onVisibility) - } + onMount(() => { + makeEventListener(document, "visibilitychange", () => { + if (document.visibilityState !== "visible") return + if (!started) return + if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return + attempt?.abort() + }) + }) onCleanup(() => { - if (typeof document !== "undefined") { - document.removeEventListener("visibilitychange", onVisibility) - } stop() abort.abort() flush() diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index aafa4fb66cd..bab3d39f386 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -1,6 +1,7 @@ import { createStore, produce } from "solid-js/store" import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" +import { makeEventListener } from "@solid-primitives/event-listener" import { useGlobalSync } from "./global-sync" import { useGlobalSDK } from "./global-sdk" import { useServer } from "./server" @@ -366,12 +367,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( flush() } - window.addEventListener("pagehide", flush) - document.addEventListener("visibilitychange", handleVisibility) + makeEventListener(window, "pagehide", flush) + makeEventListener(document, "visibilitychange", handleVisibility) onCleanup(() => { - window.removeEventListener("pagehide", flush) - document.removeEventListener("visibilitychange", handleVisibility) scroll.dispose() }) }) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index b5a96110f65..79b9abd3328 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -12,6 +12,7 @@ import { untrack, type Accessor, } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" import { useNavigate, useParams } from "@solidjs/router" import { useLayout, LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" @@ -215,18 +216,11 @@ export default function Layout(props: ParentProps) { if (document.visibilityState !== "hidden") return reset() } - window.addEventListener("pointerup", stop) - window.addEventListener("pointercancel", stop) - window.addEventListener("blur", stop) - window.addEventListener("blur", blur) - document.addEventListener("visibilitychange", hide) - onCleanup(() => { - window.removeEventListener("pointerup", stop) - window.removeEventListener("pointercancel", stop) - window.removeEventListener("blur", stop) - window.removeEventListener("blur", blur) - document.removeEventListener("visibilitychange", hide) - }) + makeEventListener(window, "pointerup", stop) + makeEventListener(window, "pointercancel", stop) + makeEventListener(window, "blur", stop) + makeEventListener(window, "blur", blur) + makeEventListener(document, "visibilitychange", hide) }) const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined) @@ -1394,8 +1388,7 @@ export default function Layout(props: ParentProps) { } handleDeepLinks(drainPendingDeepLinks(window)) - window.addEventListener(deepLinkEvent, handler as EventListener) - onCleanup(() => window.removeEventListener(deepLinkEvent, handler as EventListener)) + makeEventListener(window, deepLinkEvent, handler as EventListener) }) async function renameProject(project: LocalProject, next: string) { diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index e51161590ea..98d06fda743 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -14,6 +14,7 @@ import { onMount, untrack, } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" import { createMediaQuery } from "@solid-primitives/media" import { createResizeObserver } from "@solid-primitives/resize-observer" import { useLocal } from "@/context/local" @@ -1687,11 +1688,10 @@ export default function Page() { ) onMount(() => { - document.addEventListener("keydown", handleKeyDown) + makeEventListener(document, "keydown", handleKeyDown) }) onCleanup(() => { - document.removeEventListener("keydown", handleKeyDown) if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame) if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame) if (refreshTimer !== undefined) window.clearTimeout(refreshTimer) diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts index 0884f4cc603..eab2108687e 100644 --- a/packages/app/src/pages/session/composer/session-composer-state.ts +++ b/packages/app/src/pages/session/composer/session-composer-state.ts @@ -1,5 +1,6 @@ import { createEffect, createMemo, on, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2" import { useParams } from "@solidjs/router" import { showToast } from "@opencode-ai/ui/toast" @@ -86,8 +87,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => pull() } - window.addEventListener(composerEvent, onEvent) - onCleanup(() => window.removeEventListener(composerEvent, onEvent)) + makeEventListener(window, composerEvent, onEvent) }) const todos = createMemo((): Todo[] => { diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index 9430b70253c..cb761752361 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -1,6 +1,7 @@ -import { createEffect, createMemo, Match, on, onCleanup, Switch } from "solid-js" +import { createEffect, createMemo, createSignal, Match, on, onCleanup, Switch } from "solid-js" import { createStore } from "solid-js/store" import { Dynamic } from "solid-js/web" +import { makeEventListener } from "@solid-primitives/event-listener" import type { FileSearchHandle } from "@opencode-ai/ui/file" import { useFileComponent } from "@opencode-ai/ui/context/file" import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" @@ -59,7 +60,7 @@ function createScrollSync(input: { tab: () => string; view: ReturnType([]) const getCode = () => { const el = scroll @@ -106,17 +107,9 @@ function createScrollSync(input: { tab: () => string; view: ReturnType { const next = getCode() - if (next.length === code.length && next.every((el, i) => el === code[i])) return - - for (const item of code) { - item.removeEventListener("scroll", onCodeScroll) - } - - code = next - - for (const item of code) { - item.addEventListener("scroll", onCodeScroll) - } + const current = code() + if (next.length === current.length && next.every((el, i) => el === current[i])) return + setCode(next) } const restore = () => { @@ -128,14 +121,14 @@ function createScrollSync(input: { tab: () => string; view: ReturnType 0) { - for (const item of code) { + if (code().length > 0) { + for (const item of code()) { if (item.scrollLeft !== pos.x) item.scrollLeft = pos.x } } if (el.scrollTop !== pos.y) el.scrollTop = pos.y - if (code.length > 0) return + if (code().length > 0) return if (el.scrollLeft !== pos.x) el.scrollLeft = pos.x } @@ -149,24 +142,24 @@ function createScrollSync(input: { tab: () => string; view: ReturnType { - if (code.length === 0) sync() + if (code().length === 0) sync() save({ - x: code[0]?.scrollLeft ?? event.currentTarget.scrollLeft, + x: code()[0]?.scrollLeft ?? event.currentTarget.scrollLeft, y: event.currentTarget.scrollTop, }) } + createEffect(() => { + for (const item of code()) makeEventListener(item, "scroll", onCodeScroll) + }) + const setViewport = (el: HTMLDivElement) => { scroll = el restore() } onCleanup(() => { - for (const item of code) { - item.removeEventListener("scroll", onCodeScroll) - } - if (scrollFrame !== undefined) cancelAnimationFrame(scrollFrame) if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame) }) @@ -358,8 +351,7 @@ export function FileTabContent(props: { tab: string }) { find?.focus() } - window.addEventListener("keydown", onKeyDown, { capture: true }) - onCleanup(() => window.removeEventListener("keydown", onKeyDown, { capture: true })) + makeEventListener(window, "keydown", onKeyDown, { capture: true }) }) createEffect( diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts index 7e2c1ccf7b3..f3215f68508 100644 --- a/packages/app/src/pages/session/helpers.ts +++ b/packages/app/src/pages/session/helpers.ts @@ -1,5 +1,6 @@ import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { same } from "@/utils/same" const emptyTabs: string[] = [] @@ -171,14 +172,9 @@ export const createSizing = () => { } onMount(() => { - window.addEventListener("pointerup", stop) - window.addEventListener("pointercancel", stop) - window.addEventListener("blur", stop) - onCleanup(() => { - window.removeEventListener("pointerup", stop) - window.removeEventListener("pointercancel", stop) - window.removeEventListener("blur", stop) - }) + makeEventListener(window, "pointerup", stop) + makeEventListener(window, "pointercancel", stop) + makeEventListener(window, "blur", stop) }) onCleanup(() => { diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index 76b65a2210c..b6812864504 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -1,4 +1,5 @@ -import { createEffect, onCleanup, type JSX } from "solid-js" +import { createEffect, createSignal, onCleanup, type JSX } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" import type { FileDiff } from "@opencode-ai/sdk/v2" import { SessionReview } from "@opencode-ai/ui/session-review" import type { @@ -123,13 +124,6 @@ export function SessionReviewTab(props: SessionReviewTabProps) { onCleanup(() => { if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame) - if (scroll) { - scroll.removeEventListener("wheel", handleInteraction, { capture: true }) - scroll.removeEventListener("mousewheel", handleInteraction, { capture: true }) - scroll.removeEventListener("pointerdown", handleInteraction, { capture: true }) - scroll.removeEventListener("touchstart", handleInteraction, { capture: true }) - scroll.removeEventListener("keydown", handleInteraction, { capture: true }) - } }) return ( @@ -138,11 +132,11 @@ export function SessionReviewTab(props: SessionReviewTabProps) { empty={props.empty} scrollRef={(el) => { scroll = el - el.addEventListener("wheel", handleInteraction, { passive: true, capture: true }) - el.addEventListener("mousewheel", handleInteraction, { passive: true, capture: true }) - el.addEventListener("pointerdown", handleInteraction, { passive: true, capture: true }) - el.addEventListener("touchstart", handleInteraction, { passive: true, capture: true }) - el.addEventListener("keydown", handleInteraction, { passive: true, capture: true }) + makeEventListener(el, "wheel", handleInteraction, { passive: true, capture: true }) + makeEventListener(el, "mousewheel", handleInteraction, { passive: true, capture: true }) + makeEventListener(el, "pointerdown", handleInteraction, { passive: true, capture: true }) + makeEventListener(el, "touchstart", handleInteraction, { passive: true, capture: true }) + makeEventListener(el, "keydown", handleInteraction, { capture: true }) props.onScrollRef?.(el) queueRestore() }} diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index c663d7d671e..1161d565a75 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -1,5 +1,6 @@ import { For, Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { Tabs } from "@opencode-ai/ui/tabs" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { IconButton } from "@opencode-ai/ui/icon-button" @@ -50,12 +51,8 @@ export function TerminalPanel() { const port = window.visualViewport sync() - window.addEventListener("resize", sync) - port?.addEventListener("resize", sync) - onCleanup(() => { - window.removeEventListener("resize", sync) - port?.removeEventListener("resize", sync) - }) + makeEventListener(window, "resize", sync) + if (port) makeEventListener(port, "resize", sync) }) createEffect(() => { diff --git a/packages/ui/package.json b/packages/ui/package.json index f84454695fe..8c925753e83 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -48,6 +48,7 @@ "@pierre/diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/bounds": "0.1.3", + "@solid-primitives/event-listener": "2.4.5", "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", "@solidjs/meta": "catalog:", diff --git a/packages/ui/src/components/file.tsx b/packages/ui/src/components/file.tsx index 15915dd52d4..fb488729e27 100644 --- a/packages/ui/src/components/file.tsx +++ b/packages/ui/src/components/file.tsx @@ -16,6 +16,7 @@ import { } from "@pierre/diffs" import { type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { createMediaQuery } from "@solid-primitives/media" +import { makeEventListener } from "@solid-primitives/event-listener" import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js" import { createDefaultOptions, styleVariables } from "../pierre" import { markCommentedDiffLines, markCommentedFileLines } from "../pierre/commented-lines" @@ -286,17 +287,10 @@ function useFileViewer(config: ViewerConfig) { createEffect(() => { if (!config.enableLineSelection()) return - container.addEventListener("mousedown", handleMouseDown) - container.addEventListener("mousemove", handleMouseMove) - window.addEventListener("mouseup", handleMouseUp) - document.addEventListener("selectionchange", handleSelectionChange) - - onCleanup(() => { - container.removeEventListener("mousedown", handleMouseDown) - container.removeEventListener("mousemove", handleMouseMove) - window.removeEventListener("mouseup", handleMouseUp) - document.removeEventListener("selectionchange", handleSelectionChange) - }) + makeEventListener(container, "mousedown", handleMouseDown) + makeEventListener(container, "mousemove", handleMouseMove) + makeEventListener(window, "mouseup", handleMouseUp) + makeEventListener(document, "selectionchange", handleSelectionChange) }) onCleanup(() => { diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index 8ce45bc5c68..b5879624e03 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -1,6 +1,7 @@ import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks" -import { createEffect, For, onCleanup, type JSX, on, Show } from "solid-js" +import { createEffect, For, type JSX, on, Show } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { useI18n } from "../context/i18n" import { Icon, type IconProps } from "./icon" import { IconButton } from "./icon-button" @@ -228,9 +229,8 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) setState("stuck", rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0) } - scroll.addEventListener("scroll", handler, { passive: true }) + makeEventListener(scroll, "scroll", handler, { passive: true }) handler() - onCleanup(() => scroll.removeEventListener("scroll", handler)) }) return ( diff --git a/packages/ui/src/components/popover.tsx b/packages/ui/src/components/popover.tsx index 9d3da410909..8263640a509 100644 --- a/packages/ui/src/components/popover.tsx +++ b/packages/ui/src/components/popover.tsx @@ -1,15 +1,7 @@ import { Popover as Kobalte } from "@kobalte/core/popover" -import { - ComponentProps, - JSXElement, - ParentProps, - Show, - createEffect, - onCleanup, - splitProps, - ValidComponent, -} from "solid-js" +import { ComponentProps, JSXElement, ParentProps, Show, createEffect, splitProps, ValidComponent } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { useI18n } from "../context/i18n" import { IconButton } from "./icon-button" @@ -104,15 +96,9 @@ export function Popover(props: PopoverProps close("outside") } - window.addEventListener("keydown", onKeyDown, true) - window.addEventListener("pointerdown", onPointerDown, true) - window.addEventListener("focusin", onFocusIn, true) - - onCleanup(() => { - window.removeEventListener("keydown", onKeyDown, true) - window.removeEventListener("pointerdown", onPointerDown, true) - window.removeEventListener("focusin", onFocusIn, true) - }) + makeEventListener(window, "keydown", onKeyDown, { capture: true }) + makeEventListener(window, "pointerdown", onPointerDown, { capture: true }) + makeEventListener(window, "focusin", onFocusIn, { capture: true }) }) const content = () => ( diff --git a/packages/ui/src/context/dialog.tsx b/packages/ui/src/context/dialog.tsx index afba5f648c8..c1c56212b5f 100644 --- a/packages/ui/src/context/dialog.tsx +++ b/packages/ui/src/context/dialog.tsx @@ -12,6 +12,7 @@ import { type JSX, } from "solid-js" import { Dialog as Kobalte } from "@kobalte/core/dialog" +import { makeEventListener } from "@solid-primitives/event-listener" type DialogElement = () => JSX.Element @@ -68,8 +69,7 @@ function init() { event.stopPropagation() } - window.addEventListener("keydown", onKeyDown, true) - onCleanup(() => window.removeEventListener("keydown", onKeyDown, true)) + makeEventListener(window, "keydown", onKeyDown, { capture: true }) }) const show = (element: DialogElement, owner: Owner, onClose?: () => void) => { diff --git a/packages/ui/src/hooks/create-auto-scroll.tsx b/packages/ui/src/hooks/create-auto-scroll.tsx index 3dc520c6213..9733b094eca 100644 --- a/packages/ui/src/hooks/create-auto-scroll.tsx +++ b/packages/ui/src/hooks/create-auto-scroll.tsx @@ -1,5 +1,6 @@ -import { createEffect, on, onCleanup } from "solid-js" +import { createEffect, createSignal, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { createResizeObserver } from "@solid-primitives/resize-observer" export interface AutoScrollOptions { @@ -14,7 +15,6 @@ export function createAutoScroll(options: AutoScrollOptions) { let settling = false let settleTimer: ReturnType | undefined let autoTimer: ReturnType | undefined - let cleanup: (() => void) | undefined let auto: { top: number; time: number } | undefined const threshold = () => options.bottomThreshold ?? 10 @@ -216,26 +216,14 @@ export function createAutoScroll(options: AutoScrollOptions) { onCleanup(() => { if (settleTimer) clearTimeout(settleTimer) if (autoTimer) clearTimeout(autoTimer) - if (cleanup) cleanup() }) return { scrollRef: (el: HTMLElement | undefined) => { - if (cleanup) { - cleanup() - cleanup = undefined - } - - scroll = el - if (!el) return updateOverflowAnchor(el) - el.addEventListener("wheel", handleWheel, { passive: true }) - - cleanup = () => { - el.removeEventListener("wheel", handleWheel) - } + makeEventListener(el, "wheel", handleWheel, { passive: true }) }, contentRef: (el: HTMLElement | undefined) => setStore("contentRef", el), handleScroll, diff --git a/packages/ui/src/pierre/file-find.ts b/packages/ui/src/pierre/file-find.ts index 841b57edc5a..d1cf6dd3055 100644 --- a/packages/ui/src/pierre/file-find.ts +++ b/packages/ui/src/pierre/file-find.ts @@ -1,4 +1,5 @@ -import { createEffect, onCleanup, onMount } from "solid-js" +import { createEffect, createSignal, onCleanup, onMount } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" import { createResizeObserver } from "@solid-primitives/resize-observer" import { createStore } from "solid-js/store" @@ -105,9 +106,9 @@ type CreateFileFindOptions = { export function createFileFind(opts: CreateFileFindOptions) { let input: HTMLInputElement | undefined let overlayFrame: number | undefined - let overlayScroll: HTMLElement[] = [] let mode: "highlights" | "overlay" = "overlay" let hits: Range[] = [] + const [overlayScroll, setOverlayScroll] = createSignal([]) const [state, setState] = createStore({ open: false, @@ -123,8 +124,7 @@ export function createFileFind(opts: CreateFileFindOptions) { const pos = () => state.pos const clearOverlayScroll = () => { - for (const el of overlayScroll) el.removeEventListener("scroll", scheduleOverlay) - overlayScroll = [] + setOverlayScroll([]) } const clearOverlay = () => { @@ -197,11 +197,11 @@ export function createFileFind(opts: CreateFileFindOptions) { (node): node is HTMLElement => node instanceof HTMLElement, ) : [] - if (next.length === overlayScroll.length && next.every((el, i) => el === overlayScroll[i])) return + const current = overlayScroll() + if (next.length === current.length && next.every((el, i) => el === current[i])) return clearOverlayScroll() - overlayScroll = next - for (const el of overlayScroll) el.addEventListener("scroll", scheduleOverlay, { passive: true }) + setOverlayScroll(next) } const clearFind = () => { @@ -404,6 +404,10 @@ export function createFileFind(opts: CreateFileFindOptions) { close, } + createEffect(() => { + for (const el of overlayScroll()) makeEventListener(el, "scroll", scheduleOverlay, { passive: true }) + }) + onMount(() => { mode = supportsHighlights() ? "highlights" : "overlay" installShortcuts() @@ -425,16 +429,12 @@ export function createFileFind(opts: CreateFileFindOptions) { const update = () => positionBar() requestAnimationFrame(update) - window.addEventListener("resize", update, { passive: true }) + makeEventListener(window, "resize", update, { passive: true }) const wrapper = opts.wrapper() if (!wrapper) return const root = scrollParent(wrapper) ?? wrapper createResizeObserver(root, update) - - onCleanup(() => { - window.removeEventListener("resize", update) - }) }) onCleanup(() => { diff --git a/packages/ui/src/theme/context.tsx b/packages/ui/src/theme/context.tsx index 7d25ac39722..5664eeebd5a 100644 --- a/packages/ui/src/theme/context.tsx +++ b/packages/ui/src/theme/context.tsx @@ -1,5 +1,6 @@ -import { createEffect, onCleanup, onMount } from "solid-js" +import { createEffect, onMount } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { createSimpleContext } from "../context/helper" import oc2ThemeJson from "./themes/oc-2.json" import { resolveThemeVariant, themeToCss } from "./resolve" @@ -237,19 +238,15 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ } } - if (typeof window === "object") { - window.addEventListener("storage", onStorage) - onCleanup(() => window.removeEventListener("storage", onStorage)) - } - onMount(() => { + makeEventListener(window, "storage", onStorage) + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") const onMedia = () => { if (store.colorScheme !== "system") return setStore("mode", getSystemMode()) } - mediaQuery.addEventListener("change", onMedia) - onCleanup(() => mediaQuery.removeEventListener("change", onMedia)) + makeEventListener(mediaQuery, "change", onMedia) const rawTheme = read(STORAGE_KEYS.THEME_ID) const savedTheme = normalize(rawTheme ?? props.defaultTheme) ?? "oc-2" From ec3ae17e4d6abb9685b1d558d5e51416c9bfad60 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 2 Apr 2026 10:23:59 +0000 Subject: [PATCH 14/19] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 3c38eb881fa..23e4a310c93 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-aUDDy/Y6qBRWbCo5oFsocii1DWSsw2qRheoKEMw5E0w=", - "aarch64-linux": "sha256-mVPve3nyrlpWkaERQfvzue8BpWgIXsIg4KnXHchYg2w=", - "aarch64-darwin": "sha256-54W7ZHqH67zib+SarZyQhP2hf18hv8fKsI+QPiUGBwY=", - "x86_64-darwin": "sha256-n1yP4kEl9+a6orKUVdeLrmSxau2+pT9dTneT8ws5Fks=" + "x86_64-linux": "sha256-SQVfq41OQdGCgWuWqyqIN6aggL0r3Hzn2hJ9BwPJN+I=", + "aarch64-linux": "sha256-4w/1HhxsTzPFTHNf4JlnKle6Boz1gVTEedWG64T8E/M=", + "aarch64-darwin": "sha256-uMd+pU1u1yqP4OP/9461Tyy3zwwv/llr+rlllLjM98A=", + "x86_64-darwin": "sha256-BhIW3FPqKkM2vGfCrxXUvj5tarey33Q7dxCuaj5A+yU=" } } From 23c865608018a882a99149b7e979f4f6fbcc0600 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 2 Apr 2026 10:00:43 -0500 Subject: [PATCH 15/19] refactor: split up models.dev and config model definitions to prevent coupling (#20605) --- packages/opencode/src/config/config.ts | 85 +++++++++++++++++----- packages/opencode/src/provider/models.ts | 4 - packages/opencode/src/provider/provider.ts | 4 +- packages/opencode/src/session/llm.ts | 3 +- 4 files changed, 70 insertions(+), 26 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 3cae1af4bdb..b41e5d71c25 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -730,28 +730,77 @@ export namespace Config { }) export type Layout = z.infer + export const Model = z + .object({ + id: z.string(), + name: z.string(), + family: z.string().optional(), + release_date: z.string(), + attachment: z.boolean(), + reasoning: z.boolean(), + temperature: z.boolean(), + tool_call: z.boolean(), + interleaved: z + .union([ + z.literal(true), + z + .object({ + field: z.enum(["reasoning_content", "reasoning_details"]), + }) + .strict(), + ]) + .optional(), + cost: z + .object({ + input: z.number(), + output: z.number(), + cache_read: z.number().optional(), + cache_write: z.number().optional(), + context_over_200k: z + .object({ + input: z.number(), + output: z.number(), + cache_read: z.number().optional(), + cache_write: z.number().optional(), + }) + .optional(), + }) + .optional(), + limit: z.object({ + context: z.number(), + input: z.number().optional(), + output: z.number(), + }), + modalities: z + .object({ + input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), + output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), + }) + .optional(), + experimental: z.boolean().optional(), + status: z.enum(["alpha", "beta", "deprecated"]).optional(), + options: z.record(z.string(), z.any()), + headers: z.record(z.string(), z.string()).optional(), + provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(), + variants: z + .record( + z.string(), + z + .object({ + disabled: z.boolean().optional().describe("Disable this variant for the model"), + }) + .catchall(z.any()), + ) + .optional() + .describe("Variant-specific configuration"), + }) + .partial() + export const Provider = ModelsDev.Provider.partial() .extend({ whitelist: z.array(z.string()).optional(), blacklist: z.array(z.string()).optional(), - models: z - .record( - z.string(), - ModelsDev.Model.partial().extend({ - variants: z - .record( - z.string(), - z - .object({ - disabled: z.boolean().optional().describe("Disable this variant for the model"), - }) - .catchall(z.any()), - ) - .optional() - .describe("Variant-specific configuration"), - }), - ) - .optional(), + models: z.record(z.string(), Model).optional(), options: z .object({ apiKey: z.string().optional(), diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 30901ea74f0..0e2a26b4d98 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -61,12 +61,8 @@ export namespace ModelsDev { output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), }) .optional(), - experimental: z.boolean().optional(), status: z.enum(["alpha", "beta", "deprecated"]).optional(), - options: z.record(z.string(), z.any()), - headers: z.record(z.string(), z.string()).optional(), provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(), - variants: z.record(z.string(), z.record(z.string(), z.any())).optional(), }) export type Model = z.infer diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 441f84b907c..c1b5e61d19d 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -903,8 +903,8 @@ export namespace Provider { npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", }, status: model.status ?? "active", - headers: model.headers ?? {}, - options: model.options ?? {}, + headers: {}, + options: {}, cost: { input: model.cost?.input ?? 0, output: model.cost?.output ?? 0, diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index dc89db409e8..41f72df06a6 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -1,7 +1,6 @@ import { Provider } from "@/provider/provider" import { Log } from "@/util/log" -import { Cause, Effect, Layer, Record, ServiceMap } from "effect" -import * as Queue from "effect/Queue" +import { Effect, Layer, Record, ServiceMap } from "effect" import * as Stream from "effect/Stream" import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai" import { mergeDeep, pipe } from "remeda" From 57a5236e71f983fbeb06834fd4ec7990aa260975 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 2 Apr 2026 15:01:45 +0000 Subject: [PATCH 16/19] chore: generate --- packages/sdk/js/src/v2/gen/types.gen.ts | 12 --------- packages/sdk/openapi.json | 35 +------------------------ 2 files changed, 1 insertion(+), 46 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 290c6fd5ec8..bf9fe95203c 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4167,23 +4167,11 @@ export type ProviderListResponses = { input: Array<"text" | "audio" | "image" | "video" | "pdf"> output: Array<"text" | "audio" | "image" | "video" | "pdf"> } - experimental?: boolean status?: "alpha" | "beta" | "deprecated" - options: { - [key: string]: unknown - } - headers?: { - [key: string]: string - } provider?: { npm?: string api?: string } - variants?: { - [key: string]: { - [key: string]: unknown - } - } } } }> diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index ba7188af13f..5a60c4e4714 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -4740,29 +4740,10 @@ }, "required": ["input", "output"] }, - "experimental": { - "type": "boolean" - }, "status": { "type": "string", "enum": ["alpha", "beta", "deprecated"] }, - "options": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "headers": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - }, "provider": { "type": "object", "properties": { @@ -4773,19 +4754,6 @@ "type": "string" } } - }, - "variants": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } } }, "required": [ @@ -4796,8 +4764,7 @@ "reasoning", "temperature", "tool_call", - "limit", - "options" + "limit" ] } } From 291a857fb841725e1e34640f8c52b415092943b9 Mon Sep 17 00:00:00 2001 From: Noam Bressler Date: Thu, 2 Apr 2026 18:18:16 +0300 Subject: [PATCH 17/19] feat: add optional messageID to ShellInput (#20657) --- packages/opencode/src/session/prompt.ts | 3 ++- packages/sdk/js/src/v2/gen/sdk.gen.ts | 2 ++ packages/sdk/js/src/v2/gen/types.gen.ts | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 436847ed4e3..5121f245275 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -756,7 +756,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the } const model = input.model ?? agent.model ?? (yield* lastModel(input.sessionID)) const userMsg: MessageV2.User = { - id: MessageID.ascending(), + id: input.messageID ?? MessageID.ascending(), sessionID: input.sessionID, time: { created: Date.now() }, role: "user", @@ -1827,6 +1827,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the export const ShellInput = z.object({ sessionID: SessionID.zod, + messageID: MessageID.zod.optional(), agent: z.string(), model: z .object({ diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 527584e7e29..113b3ed0f84 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -2231,6 +2231,7 @@ export class Session2 extends HeyApiClient { sessionID: string directory?: string workspace?: string + messageID?: string agent?: string model?: { providerID: string @@ -2248,6 +2249,7 @@ export class Session2 extends HeyApiClient { { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, { in: "body", key: "agent" }, { in: "body", key: "model" }, { in: "body", key: "command" }, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index bf9fe95203c..6ac6af1439a 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -3815,6 +3815,7 @@ export type SessionCommandResponse = SessionCommandResponses[keyof SessionComman export type SessionShellData = { body?: { + messageID?: string agent: string model?: { providerID: string From 159ede2d5c68ce0e2c2662cc1f89cc9d01a0299e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 2 Apr 2026 15:19:26 +0000 Subject: [PATCH 18/19] chore: generate --- packages/sdk/openapi.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 5a60c4e4714..549fcc4fe14 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -3942,6 +3942,10 @@ "schema": { "type": "object", "properties": { + "messageID": { + "type": "string", + "pattern": "^msg.*" + }, "agent": { "type": "string" }, From 510a1e81403c2438498a5d48b61ce4aa998dfcc9 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 2 Apr 2026 10:38:30 -0500 Subject: [PATCH 19/19] ignore: fix typecheck in dev (#20702) --- packages/opencode/src/config/config.ts | 85 +++++----------------- packages/opencode/src/provider/models.ts | 4 + packages/opencode/src/provider/provider.ts | 4 +- packages/opencode/src/session/llm.ts | 3 +- packages/sdk/js/src/v2/gen/types.gen.ts | 12 +++ packages/sdk/openapi.json | 35 ++++++++- 6 files changed, 72 insertions(+), 71 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index b41e5d71c25..3cae1af4bdb 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -730,77 +730,28 @@ export namespace Config { }) export type Layout = z.infer - export const Model = z - .object({ - id: z.string(), - name: z.string(), - family: z.string().optional(), - release_date: z.string(), - attachment: z.boolean(), - reasoning: z.boolean(), - temperature: z.boolean(), - tool_call: z.boolean(), - interleaved: z - .union([ - z.literal(true), - z - .object({ - field: z.enum(["reasoning_content", "reasoning_details"]), - }) - .strict(), - ]) - .optional(), - cost: z - .object({ - input: z.number(), - output: z.number(), - cache_read: z.number().optional(), - cache_write: z.number().optional(), - context_over_200k: z - .object({ - input: z.number(), - output: z.number(), - cache_read: z.number().optional(), - cache_write: z.number().optional(), - }) - .optional(), - }) - .optional(), - limit: z.object({ - context: z.number(), - input: z.number().optional(), - output: z.number(), - }), - modalities: z - .object({ - input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), - output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), - }) - .optional(), - experimental: z.boolean().optional(), - status: z.enum(["alpha", "beta", "deprecated"]).optional(), - options: z.record(z.string(), z.any()), - headers: z.record(z.string(), z.string()).optional(), - provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(), - variants: z - .record( - z.string(), - z - .object({ - disabled: z.boolean().optional().describe("Disable this variant for the model"), - }) - .catchall(z.any()), - ) - .optional() - .describe("Variant-specific configuration"), - }) - .partial() - export const Provider = ModelsDev.Provider.partial() .extend({ whitelist: z.array(z.string()).optional(), blacklist: z.array(z.string()).optional(), - models: z.record(z.string(), Model).optional(), + models: z + .record( + z.string(), + ModelsDev.Model.partial().extend({ + variants: z + .record( + z.string(), + z + .object({ + disabled: z.boolean().optional().describe("Disable this variant for the model"), + }) + .catchall(z.any()), + ) + .optional() + .describe("Variant-specific configuration"), + }), + ) + .optional(), options: z .object({ apiKey: z.string().optional(), diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 0e2a26b4d98..30901ea74f0 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -61,8 +61,12 @@ export namespace ModelsDev { output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), }) .optional(), + experimental: z.boolean().optional(), status: z.enum(["alpha", "beta", "deprecated"]).optional(), + options: z.record(z.string(), z.any()), + headers: z.record(z.string(), z.string()).optional(), provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(), + variants: z.record(z.string(), z.record(z.string(), z.any())).optional(), }) export type Model = z.infer diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c1b5e61d19d..441f84b907c 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -903,8 +903,8 @@ export namespace Provider { npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", }, status: model.status ?? "active", - headers: {}, - options: {}, + headers: model.headers ?? {}, + options: model.options ?? {}, cost: { input: model.cost?.input ?? 0, output: model.cost?.output ?? 0, diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 41f72df06a6..dc89db409e8 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -1,6 +1,7 @@ import { Provider } from "@/provider/provider" import { Log } from "@/util/log" -import { Effect, Layer, Record, ServiceMap } from "effect" +import { Cause, Effect, Layer, Record, ServiceMap } from "effect" +import * as Queue from "effect/Queue" import * as Stream from "effect/Stream" import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai" import { mergeDeep, pipe } from "remeda" diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 6ac6af1439a..2f8e99cfed8 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4168,11 +4168,23 @@ export type ProviderListResponses = { input: Array<"text" | "audio" | "image" | "video" | "pdf"> output: Array<"text" | "audio" | "image" | "video" | "pdf"> } + experimental?: boolean status?: "alpha" | "beta" | "deprecated" + options: { + [key: string]: unknown + } + headers?: { + [key: string]: string + } provider?: { npm?: string api?: string } + variants?: { + [key: string]: { + [key: string]: unknown + } + } } } }> diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 549fcc4fe14..8e41f9deb0b 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -4744,10 +4744,29 @@ }, "required": ["input", "output"] }, + "experimental": { + "type": "boolean" + }, "status": { "type": "string", "enum": ["alpha", "beta", "deprecated"] }, + "options": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "headers": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, "provider": { "type": "object", "properties": { @@ -4758,6 +4777,19 @@ "type": "string" } } + }, + "variants": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } } }, "required": [ @@ -4768,7 +4800,8 @@ "reasoning", "temperature", "tool_call", - "limit" + "limit", + "options" ] } }