From 50fd416d49c86451462e8d49c9fea1b3f5584d93 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 27 Nov 2025 05:05:55 -0600 Subject: [PATCH 01/27] fix: simpler sanitize --- packages/ui/src/components/message-part.tsx | 36 +++++++-------------- packages/util/src/sanitize.ts | 2 +- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 40740fa1f..74aa3d98a 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -17,7 +17,7 @@ import { Diff } from "./diff" import { DiffChanges } from "./diff-changes" import { Markdown } from "./markdown" import { getDirectory, getFilename } from "@opencode-ai/util/path" -import { sanitize, sanitizePart } from "@opencode-ai/util/sanitize" +import { sanitizePart } from "@opencode-ai/util/sanitize" export interface MessageProps { message: MessageType @@ -83,15 +83,10 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp export function Part(props: MessagePartProps) { const component = createMemo(() => PART_MAPPING[props.part.type]) + const part = createMemo(() => sanitizePart(props.part, props.sanitize)) return ( - + ) } @@ -102,7 +97,6 @@ export interface ToolProps { tool: string output?: string hideDetails?: boolean - sanitize?: RegExp } export type ToolComponent = Component @@ -170,7 +164,6 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { metadata={metadata} output={part.state.status === "completed" ? part.state.output : undefined} hideDetails={props.hideDetails} - sanitize={props.sanitize} /> @@ -211,7 +204,7 @@ ToolRegistry.register({ icon="glasses" trigger={{ title: "Read", - subtitle: props.input.filePath ? getFilename(sanitize(props.input.filePath, props.sanitize)) : "", + subtitle: props.input.filePath ? getFilename(props.input.filePath) : "", }} /> ) @@ -222,12 +215,9 @@ ToolRegistry.register({ name: "list", render(props) { return ( - + -
{sanitize(props.output, props.sanitize)}
+
{props.output}
) @@ -335,7 +325,7 @@ ToolRegistry.register({ >
@@ -355,13 +345,9 @@ ToolRegistry.register({
Edit
- - {getDirectory(sanitize(props.input.filePath!, props.sanitize))} - + {getDirectory(props.input.filePath!)} - - {getFilename(sanitize(props.input.filePath ?? "", props.sanitize))} - + {getFilename(props.input.filePath ?? "")}
@@ -376,11 +362,11 @@ ToolRegistry.register({
diff --git a/packages/util/src/sanitize.ts b/packages/util/src/sanitize.ts index 270b618ae..38ad2b290 100644 --- a/packages/util/src/sanitize.ts +++ b/packages/util/src/sanitize.ts @@ -2,7 +2,7 @@ import type { Part } from "@opencode-ai/sdk/client" export const sanitize = (text: string | undefined, remove?: RegExp) => (remove ? text?.replace(remove, "") : text) ?? "" -export const sanitizePart = (part: Part, remove: RegExp) => { +export const sanitizePart = (part: Part, remove: RegExp | undefined) => { if (part.type === "text") { part.text = sanitize(part.text, remove) } else if (part.type === "reasoning") { From d6ef47bb2d9b2c507a16d5944edbf32837d05f59 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 27 Nov 2025 11:06:46 +0000 Subject: [PATCH 02/27] chore: format code --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index d4875c3a3..5cf3c465b 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -24,4 +24,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 1b9dc0fcf..a0c125c68 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -26,4 +26,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From feb1f36126b812f3cff0dcddca5060977c1cb00a Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 27 Nov 2025 05:25:31 -0600 Subject: [PATCH 03/27] fix: session turn margins --- packages/desktop/src/pages/session.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 60f9e9ef5..fd61c15ec 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -340,7 +340,7 @@ export default function Page() { 1}> <>
From c120447fd0e1b319d9f6fe52afae63dc2b4739f9 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 27 Nov 2025 05:41:50 -0600 Subject: [PATCH 04/27] fix: desktop layout and scroll gutters --- packages/desktop/src/pages/session.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index fd61c15ec..2b7374508 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -333,7 +333,12 @@ export default function Page() { flex: layout.review.state() === "pane", }} > -
+
@@ -350,7 +355,7 @@ export default function Page() {
-
+
New session
From 049510afbd3a091f30091c4de3d4162a13808690 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 27 Nov 2025 12:04:30 +0000 Subject: [PATCH 05/27] ignore: update download stats 2025-11-27 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index b58a8f1c5..ab7f7413c 100644 --- a/STATS.md +++ b/STATS.md @@ -152,3 +152,4 @@ | 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) | | 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) | | 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) | +| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) | From 70dd6dd3945d2b991de91c12c7434cd0ddb03a01 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 27 Nov 2025 09:58:57 -0500 Subject: [PATCH 06/27] doc: slashing kimi k2 thinking price --- packages/web/src/content/docs/zen.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index f672662ed..5632bcca4 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -108,8 +108,8 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | Big Pickle | Free | Free | Free | - | | Grok Code Fast 1 | Free | Free | Free | - | | GLM 4.6 | $0.60 | $2.20 | $0.10 | - | -| Kimi K2 | $0.45 | $2.50 | - | - | -| Kimi K2 Thinking | $0.60 | $2.50 | - | - | +| Kimi K2 | $0.40 | $2.50 | - | - | +| Kimi K2 Thinking | $0.40 | $2.50 | - | - | | Qwen3 Coder 480B | $0.45 | $1.50 | - | - | | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | From ea7c213f5d3d09bcc390ccf678b282645c35311f Mon Sep 17 00:00:00 2001 From: Albert O'Shea Date: Fri, 28 Nov 2025 03:05:51 +1100 Subject: [PATCH 07/27] nix: fix workflow failing on PRs (#4820) Co-authored-by: Github Action --- .github/workflows/update-nix-hashes.yml | 2 ++ flake.lock | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index 38dba2e35..1f1ca0e80 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -18,6 +18,7 @@ on: jobs: update: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest env: SYSTEM: x86_64-linux @@ -29,6 +30,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 ref: ${{ github.head_ref || github.ref_name }} + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - name: Setup Nix uses: DeterminateSystems/nix-installer-action@v20 diff --git a/flake.lock b/flake.lock index b0749bea4..5653a5af3 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1764138170, - "narHash": "sha256-2bCmfCUZyi2yj9FFXYKwsDiaZmizN75cLhI/eWmf3tk=", + "lastModified": 1764167966, + "narHash": "sha256-nXv6xb7cq+XpjBYIjWEGTLCqQetxJu6zvVlrqHMsCOA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "bb813de6d2241bcb1b5af2d3059f560c66329967", + "rev": "5c46f3bd98147c8d82366df95bbef2cab3a967ea", "type": "github" }, "original": { From 35d118b0c45b20fdedb1dd120559e5d7e15e4283 Mon Sep 17 00:00:00 2001 From: Jay V Date: Thu, 27 Nov 2025 12:12:44 -0500 Subject: [PATCH 08/27] ignore: add reply-to support for enterprise form emails --- packages/console/app/src/routes/api/enterprise.ts | 1 + packages/console/core/src/aws.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/console/app/src/routes/api/enterprise.ts b/packages/console/app/src/routes/api/enterprise.ts index e33737d57..6776a7b3c 100644 --- a/packages/console/app/src/routes/api/enterprise.ts +++ b/packages/console/app/src/routes/api/enterprise.ts @@ -36,6 +36,7 @@ ${body.email}`.trim() to: "contact@anoma.ly", subject: `Enterprise Inquiry from ${body.name}`, body: emailContent, + replyTo: body.email, }) return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 }) diff --git a/packages/console/core/src/aws.ts b/packages/console/core/src/aws.ts index e87ada6ef..a4c151086 100644 --- a/packages/console/core/src/aws.ts +++ b/packages/console/core/src/aws.ts @@ -22,6 +22,7 @@ export namespace AWS { to: z.string(), subject: z.string(), body: z.string(), + replyTo: z.string().optional(), }), async (input) => { const res = await createClient().fetch("https://email.us-east-1.amazonaws.com/v2/email/outbound-emails", { @@ -35,6 +36,7 @@ export namespace AWS { Destination: { ToAddresses: [input.to], }, + ...(input.replyTo && { ReplyToAddresses: [input.replyTo] }), Content: { Simple: { Subject: { From 5a50d54fdad9297eae40857576c86f467a969356 Mon Sep 17 00:00:00 2001 From: Jay V Date: Thu, 27 Nov 2025 12:14:50 -0500 Subject: [PATCH 09/27] ignore: lock --- bun.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/bun.lock b/bun.lock index 9ea4c7de3..8802d54c6 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "opencode", From ea52ed41beccc7f516de5d562663f17ccd9a2e01 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 27 Nov 2025 17:15:41 +0000 Subject: [PATCH 10/27] chore: format code --- bun.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/bun.lock b/bun.lock index 8802d54c6..0a41d76ff 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "opencode", From a0b689c1404542c574abeda60cc05b985a7861ba Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 27 Nov 2025 13:42:26 -0500 Subject: [PATCH 11/27] tui: hide favorite keybind in model dialog when disconnected to prevent errors --- packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx | 5 +++-- packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index ba1dc70b2..bfb27d0ca 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -21,7 +21,7 @@ export function DialogModel() { const options = createMemo(() => { const query = ref()?.filter - const favorites = local.model.favorite() + const favorites = connected() ? local.model.favorite() : [] const recents = local.model.recent() const currentModel = local.model.current() @@ -67,7 +67,7 @@ export function DialogModel() { modelID: model.id, }, title: model.name ?? item.modelID, - description: `${provider.name} ★`, + description: provider.name, category: "Favorites", disabled: provider.id === "opencode" && model.id.includes("-nano"), footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, @@ -204,6 +204,7 @@ export function DialogModel() { { keybind: Keybind.parse("ctrl+f")[0], title: "Favorite", + disabled: !connected(), onTrigger: (option) => { local.model.toggleFavorite(option.value as { providerID: string; modelID: string }) }, diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index b8d2a5b14..ca86a1986 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -21,6 +21,7 @@ export interface DialogSelectProps { keybind?: { keybind: Keybind.Info title: string + disabled?: boolean onTrigger: (option: DialogSelectOption) => void }[] current?: T @@ -150,6 +151,7 @@ export function DialogSelect(props: DialogSelectProps) { } for (const item of props.keybind ?? []) { + if (item.disabled) continue if (Keybind.match(item.keybind, keybind.parse(evt))) { const s = selected() if (s) { @@ -254,7 +256,7 @@ export function DialogSelect(props: DialogSelectProps) { - + !x.disabled)}> {(item) => ( From 95b667d21e718165893236fb5dd6859664fc394e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 27 Nov 2025 13:48:34 -0500 Subject: [PATCH 12/27] tui: remove cancel keybind hint from prompt dialog to simplify UI --- packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx | 5 +---- packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx | 3 --- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index bfb27d0ca..f43fc1e79 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -137,13 +137,10 @@ export function DialogModel() { providerID: provider.id, modelID: model, } - const favorite = favorites.some( - (item) => item.providerID === value.providerID && item.modelID === value.modelID, - ) return { value, title: info.name ?? model, - description: connected() ? `${provider.name}${favorite ? " ★" : ""}` : undefined, + description: connected() ? provider.name : undefined, category: connected() ? provider.name : undefined, disabled: provider.id === "opencode" && model.includes("-nano"), footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx index 83f8e27fc..9ae370658 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx @@ -55,9 +55,6 @@ export function DialogPrompt(props: DialogPromptProps) { enter submit - - esc cancel - ) From 9ecaf618db19e8ce92ae84685a94205e9c0ebcbe Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 27 Nov 2025 13:54:42 -0500 Subject: [PATCH 13/27] tui: fix provider sorting to prioritize recommended options --- .../opencode/src/cli/cmd/tui/component/dialog-provider.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 30a8bb2fc..9a53abbbe 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -26,6 +26,7 @@ export function createDialogProviderOptions() { const options = createMemo(() => { return pipe( sync.data.provider_next.all, + sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99), map((provider) => ({ title: provider.name, value: provider.id, @@ -33,6 +34,7 @@ export function createDialogProviderOptions() { opencode: "Recommended", anthropic: "Claude Max or API key", }[provider.id], + category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other", async onSelect() { const methods = sync.data.provider_auth[provider.id] ?? [ { @@ -85,7 +87,6 @@ export function createDialogProviderOptions() { } }, })), - sortBy((x) => PROVIDER_PRIORITY[x.value] ?? 99), ) }) return options From 58544558150d7b1d0c186cd5e79fff50ffc53459 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 27 Nov 2025 14:09:53 -0500 Subject: [PATCH 14/27] tui: improve provider dialog text clarity for better user guidance --- .../opencode/src/cli/cmd/tui/component/dialog-model.tsx | 2 +- .../opencode/src/cli/cmd/tui/component/dialog-provider.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index f43fc1e79..0a7cc5374 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -193,7 +193,7 @@ export function DialogModel() { keybind={[ { keybind: { ctrl: true, name: "a", meta: false, shift: false, leader: false }, - title: connected() ? "Connect provider" : "More providers", + title: connected() ? "Connect provider" : "View more providers", onTrigger() { dialog.replace(() => ) }, diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 9a53abbbe..8ba7845f2 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -30,9 +30,9 @@ export function createDialogProviderOptions() { map((provider) => ({ title: provider.name, value: provider.id, - footer: { - opencode: "Recommended", - anthropic: "Claude Max or API key", + description: { + opencode: "(Recommended)", + anthropic: "(Claude Max or API key)", }[provider.id], category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other", async onSelect() { From 350982e6361612c57600f606208a5b3dfe217498 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 27 Nov 2025 14:38:51 -0500 Subject: [PATCH 15/27] tui: simplify model dialog ordering logic to reduce complexity --- .../cli/cmd/tui/component/dialog-model.tsx | 102 +++++++----------- .../src/cli/cmd/tui/ui/dialog-select.tsx | 30 +++--- 2 files changed, 58 insertions(+), 74 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index 0a7cc5374..6580c683e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -6,6 +6,7 @@ import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select" import { useDialog } from "@tui/ui/dialog" import { createDialogProviderOptions, DialogProvider } from "./dialog-provider" import { Keybind } from "@/util/keybind" +import { iife } from "@/util/iife" export function DialogModel() { const local = useLocal() @@ -23,71 +24,46 @@ export function DialogModel() { const query = ref()?.filter const favorites = connected() ? local.model.favorite() : [] const recents = local.model.recent() - const currentModel = local.model.current() - const orderedRecents = currentModel - ? [ - currentModel, - ...recents.filter( - (item) => item.providerID !== currentModel.providerID || item.modelID !== currentModel.modelID, - ), - ] - : recents - - const isCurrent = (item: { providerID: string; modelID: string }) => - currentModel && item.providerID === currentModel.providerID && item.modelID === currentModel.modelID - - const currentIsFavorite = currentModel && favorites.some((fav) => isCurrent(fav)) - - const recentList = orderedRecents + const recentList = recents .filter((item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID)) .slice(0, 5) - const orderedFavorites = currentModel - ? [...favorites.filter((item) => isCurrent(item)), ...favorites.filter((item) => !isCurrent(item))] - : favorites - - const orderedRecentList = - currentModel && !currentIsFavorite - ? [...recentList.filter((item) => isCurrent(item)), ...recentList.filter((item) => !isCurrent(item))] - : recentList - - const favoriteOptions = - !query && favorites.length > 0 - ? orderedFavorites.flatMap((item) => { - const provider = sync.data.provider.find((x) => x.id === item.providerID) - if (!provider) return [] - const model = provider.models[item.modelID] - if (!model) return [] - return [ - { - key: item, - value: { - providerID: provider.id, - modelID: model.id, - }, - title: model.name ?? item.modelID, - description: provider.name, - category: "Favorites", - disabled: provider.id === "opencode" && model.id.includes("-nano"), - footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, - onSelect: () => { - dialog.clear() - local.model.set( - { - providerID: provider.id, - modelID: model.id, - }, - { recent: true }, - ) - }, + const favoriteOptions = !query + ? favorites.flatMap((item) => { + const provider = sync.data.provider.find((x) => x.id === item.providerID) + if (!provider) return [] + const model = provider.models[item.modelID] + if (!model) return [] + return [ + { + key: item, + value: { + providerID: provider.id, + modelID: model.id, }, - ] - }) - : [] + title: model.name ?? item.modelID, + description: provider.name, + category: "Favorites", + disabled: provider.id === "opencode" && model.id.includes("-nano"), + footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, + onSelect: () => { + dialog.clear() + local.model.set( + { + providerID: provider.id, + modelID: model.id, + }, + { recent: true }, + ) + }, + }, + ] + }) + : [] const recentOptions = !query - ? orderedRecentList.flatMap((item) => { + ? recentList.flatMap((item) => { const provider = sync.data.provider.find((x) => x.id === item.providerID) if (!provider) return [] const model = provider.models[item.modelID] @@ -140,7 +116,11 @@ export function DialogModel() { return { value, title: info.name ?? model, - description: connected() ? provider.name : undefined, + description: favorites.some( + (item) => item.providerID === value.providerID && item.modelID === value.modelID, + ) + ? "(Favorite)" + : undefined, category: connected() ? provider.name : undefined, disabled: provider.id === "opencode" && model.includes("-nano"), footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, @@ -162,10 +142,10 @@ export function DialogModel() { const inFavorites = favorites.some( (item) => item.providerID === value.providerID && item.modelID === value.modelID, ) - const inRecents = orderedRecents.some( + if (inFavorites) return false + const inRecents = recents.some( (item) => item.providerID === value.providerID && item.modelID === value.modelID, ) - if (inFavorites) return false if (inRecents) return false return true }), diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index ca86a1986..0b15340b2 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -173,8 +173,10 @@ export function DialogSelect(props: DialogSelectProps) { } props.ref?.(ref) + const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled) ?? []) + return ( - + @@ -255,18 +257,20 @@ export function DialogSelect(props: DialogSelectProps) { )} - - !x.disabled)}> - {(item) => ( - - - {item.title}{" "} - - {Keybind.toString(item.keybind)} - - )} - - + }> + + + {(item) => ( + + + {item.title}{" "} + + {Keybind.toString(item.keybind)} + + )} + + + ) } From f385524f48f03e9f019b1a12ee22a6ac00d9a375 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 27 Nov 2025 15:48:16 -0500 Subject: [PATCH 16/27] fix lock --- bun.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index 0a41d76ff..9ea4c7de3 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,6 @@ { "lockfileVersion": 1, - "configVersion": 0, + "configVersion": 1, "workspaces": { "": { "name": "opencode", From 776091cc232a37656eb9087d76c64b2e67c401c1 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 27 Nov 2025 15:50:23 -0500 Subject: [PATCH 17/27] ci: add bun version check to pre-push hook to ensure version consistency --- .husky/pre-push | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.husky/pre-push b/.husky/pre-push index b26017ee9..2fd039d56 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,2 +1,9 @@ #!/bin/sh +# Check if bun version matches package.json +EXPECTED_VERSION=$(grep '"packageManager"' package.json | sed 's/.*"bun@\([^"]*\)".*/\1/') +CURRENT_VERSION=$(bun --version) +if [ "$CURRENT_VERSION" != "$EXPECTED_VERSION" ]; then + echo "Error: Bun version $CURRENT_VERSION does not match expected version $EXPECTED_VERSION from package.json" + exit 1 +fi bun typecheck From 6a1552f65c041d45be3eb561bebf0faab759ec30 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:15:31 -0600 Subject: [PATCH 18/27] fix: unwrap solid store part --- packages/ui/src/components/message-part.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 74aa3d98a..1c2ba97f3 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -18,6 +18,7 @@ import { DiffChanges } from "./diff-changes" import { Markdown } from "./markdown" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { sanitizePart } from "@opencode-ai/util/sanitize" +import { unwrap } from "solid-js/store" export interface MessageProps { message: MessageType @@ -83,7 +84,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp export function Part(props: MessagePartProps) { const component = createMemo(() => PART_MAPPING[props.part.type]) - const part = createMemo(() => sanitizePart(props.part, props.sanitize)) + const part = createMemo(() => sanitizePart(unwrap(props.part), props.sanitize)) return ( @@ -175,7 +176,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { PART_MAPPING["text"] = function TextPartDisplay(props) { const part = props.part as TextPart - const sanitized = createMemo(() => (props.sanitize ? (sanitizePart(part, props.sanitize) as TextPart) : part)) + const sanitized = createMemo(() => (props.sanitize ? (sanitizePart(unwrap(part), props.sanitize) as TextPart) : part)) return (
From a8985b1849fe87c5896229cd547fbb04540fc704 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:15:46 -0600 Subject: [PATCH 19/27] fix(desktop): layout --- packages/desktop/src/pages/session.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 2b7374508..55dd206fb 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -345,7 +345,7 @@ export default function Page() { 1}> <>
From cc78d50ef61cfc822a47f0e895f0415ca1e0b9dc Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 28 Nov 2025 00:25:16 -0600 Subject: [PATCH 20/27] bump anthropic package --- bun.lock | 6 +++--- packages/opencode/package.json | 2 +- packages/opencode/src/session/processor.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index 9ea4c7de3..ba18de13b 100644 --- a/bun.lock +++ b/bun.lock @@ -217,7 +217,7 @@ "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.5.1", "@ai-sdk/amazon-bedrock": "3.0.57", - "@ai-sdk/anthropic": "2.0.45", + "@ai-sdk/anthropic": "2.0.50", "@ai-sdk/azure": "2.0.73", "@ai-sdk/google": "2.0.42", "@ai-sdk/google-vertex": "3.0.74", @@ -4073,7 +4073,7 @@ "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.45", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Ipv62vavDCmrV/oE/lXehL9FzwQuZOnnlhPEftWizx464Wb6lvnBTJx8uhmEYruFSzOWTI95Z33ncZ4tA8E6RQ=="], + "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="], "opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="], @@ -4619,7 +4619,7 @@ "jsonwebtoken/jws/jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="], - "opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="], + "opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="], "opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index e0909c194..ae380692d 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -43,7 +43,7 @@ "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.5.1", "@ai-sdk/amazon-bedrock": "3.0.57", - "@ai-sdk/anthropic": "2.0.45", + "@ai-sdk/anthropic": "2.0.50", "@ai-sdk/azure": "2.0.73", "@ai-sdk/google": "2.0.42", "@ai-sdk/google-vertex": "3.0.74", diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 5bd833c0f..2f2ba4e94 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -333,7 +333,7 @@ export namespace SessionProcessor { error: e, }) const error = MessageV2.fromError(e, { providerID: input.providerID }) - if ((error?.name === "APIError" && error.data.isRetryable) || error.data.message.includes("Overloaded")) { + if (error?.name === "APIError" && error.data.isRetryable) { attempt++ const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined) SessionStatus.set(input.sessionID, { From 13f89fdb8febce3995070f87024705b20c682261 Mon Sep 17 00:00:00 2001 From: DS <78942835+Tarquinen@users.noreply.github.com> Date: Fri, 28 Nov 2025 01:26:48 -0500 Subject: [PATCH 21/27] fix: filter empty messages in toModelMessage (#4811) --- packages/opencode/src/session/message-v2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 1a9b08d12..718e90921 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -666,7 +666,7 @@ export namespace MessageV2 { } } - return convertToModelMessages(result) + return convertToModelMessages(result.filter((msg) => msg.parts.length > 0)) } export const stream = fn(Identifier.schema("session"), async function* (sessionID) { From 025a47d01f83d4843c3e95ebf0a77a4b1ebd1f0c Mon Sep 17 00:00:00 2001 From: Github Action Date: Fri, 28 Nov 2025 06:27:48 +0000 Subject: [PATCH 22/27] Update Nix flake.lock and hashes --- flake.lock | 6 +++--- nix/hashes.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index 5653a5af3..b67bb39a7 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1764167966, - "narHash": "sha256-nXv6xb7cq+XpjBYIjWEGTLCqQetxJu6zvVlrqHMsCOA=", + "lastModified": 1764230294, + "narHash": "sha256-Z63xl5Scj3Y/zRBPAWq1eT68n2wBWGCIEF4waZ0bQBE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5c46f3bd98147c8d82366df95bbef2cab3a967ea", + "rev": "0d59e0290eefe0f12512043842d7096c4070f30e", "type": "github" }, "original": { diff --git a/nix/hashes.json b/nix/hashes.json index 1f11430f2..08fee7014 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-dTGBX5mde/hQP36MSFwq3G81OdwpcYRl8bcjLpesbPw=" + "nodeModules": "sha256-RHAcxfg1XmbGhft9kT+NA2JOan3yVKD76U1zV0cVIow=" } From 7112a706b8c9bb557d0e7e583bddaa63cbb2b522 Mon Sep 17 00:00:00 2001 From: Christoph Date: Fri, 28 Nov 2025 07:33:45 +0100 Subject: [PATCH 23/27] core: add built-in Dart LSP server and formatter (#4841) --- packages/opencode/src/format/formatter.ts | 9 +++++++++ packages/opencode/src/lsp/server.ts | 18 ++++++++++++++++++ packages/web/src/content/docs/formatters.mdx | 1 + packages/web/src/content/docs/lsp.mdx | 1 + 4 files changed, 29 insertions(+) diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index d1bff181f..404898080 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -246,3 +246,12 @@ export const htmlbeautifier: Info = { return Bun.which("htmlbeautifier") !== null }, } + +export const dart: Info = { + name: "dart", + command: ["dart", "format", "$FILE"], + extensions: [".dart"], + async enabled() { + return Bun.which("dart") !== null + }, +} diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 0bc229e97..ce2fbfa69 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1166,4 +1166,22 @@ export namespace LSPServer { } }, } + + export const Dart: Info = { + id: "dart", + extensions: [".dart"], + root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]), + async spawn(root) { + const dart = Bun.which("dart") + if (!dart) { + log.info("dart not found, please install dart first") + return + } + return { + process: spawn(dart, ["language-server", "--lsp"], { + cwd: root, + }), + } + }, + } } diff --git a/packages/web/src/content/docs/formatters.mdx b/packages/web/src/content/docs/formatters.mdx index 9fc41a53d..cc5cb6056 100644 --- a/packages/web/src/content/docs/formatters.mdx +++ b/packages/web/src/content/docs/formatters.mdx @@ -26,6 +26,7 @@ OpenCode comes with several built-in formatters for popular languages and framew | standardrb | .rb, .rake, .gemspec, .ru | `standardrb` command available | | htmlbeautifier | .erb, .html.erb | `htmlbeautifier` command available | | air | .R | `air` command available | +| dart | .dart | `dart` command available | So if your project has `prettier` in your `package.json`, OpenCode will automatically use it. diff --git a/packages/web/src/content/docs/lsp.mdx b/packages/web/src/content/docs/lsp.mdx index 5c12f03f6..af9f2cfc1 100644 --- a/packages/web/src/content/docs/lsp.mdx +++ b/packages/web/src/content/docs/lsp.mdx @@ -32,6 +32,7 @@ OpenCode comes with several built-in LSP servers for popular languages: | lua-ls | .lua | Auto-installs for Lua projects | | sourcekit-lsp | .swift, .objc, .objcpp | `swift` installed (`xcode` on macOS) | | php intelephense | .php | Auto-installs for PHP projects | +| dart | .dart | `dart` command available | LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met. From cb2dd34a5e6d24e6e25e58b44db10fd823b83bc4 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:30:00 -0600 Subject: [PATCH 24/27] fix: unified diff as default --- packages/ui/src/components/pierre.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/pierre.ts b/packages/ui/src/components/pierre.ts index ef01318de..5821697c7 100644 --- a/packages/ui/src/components/pierre.ts +++ b/packages/ui/src/components/pierre.ts @@ -6,7 +6,7 @@ export function createDefaultOptions(style: FileDiffOptions["diffStyle"]) themeType: "system", disableLineNumbers: false, overflow: "wrap", - diffStyle: style, + diffStyle: style ?? "unified", diffIndicators: "bars", disableBackground: false, expansionLineCount: 20, From 5efeaae093a5b64478b285b9f03098f3b096f0ba Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 28 Nov 2025 05:35:30 -0600 Subject: [PATCH 25/27] fix: desktop and share layouts --- packages/desktop/src/pages/session.tsx | 24 +++--- .../enterprise/src/routes/share/[shareID].tsx | 73 ++++++++++--------- 2 files changed, 50 insertions(+), 47 deletions(-) diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 55dd206fb..c990bf87f 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -336,7 +336,7 @@ export default function Page() {
@@ -355,7 +355,7 @@ export default function Page() {
-
+
New session
@@ -399,12 +399,14 @@ export default function Page() {
-
- { - inputRef = el - }} - /> +
+
+ { + inputRef = el + }} + /> +
@@ -507,7 +509,7 @@ export default function Page() { -
+
{ inputRef = el diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index 271fb290a..7c4af8ac2 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -171,7 +171,7 @@ export default function () { }) const title = () => ( -
+
@@ -215,7 +215,6 @@ export default function () { ) const wide = createMemo(() => diffs().length === 0) - const columnPadding = () => (wide() ? "px-6" : "px-21 @4xl:px-6") return (
@@ -243,44 +242,44 @@ export default function () {
-