@@ -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 () {
-
+
-
{title()}
+
+ {title()}
+
+ messages={messages()}
+ current={activeMessage()}
+ onMessageSelect={setActiveMessage}
+ size={wide() ? "normal" : "compact"}
+ />
>
-
+
@@ -313,7 +312,7 @@ export default function () {
0}>
-
+
Session
@@ -344,7 +343,9 @@ export default function () {
- {turns()}
+
+ {turns()}
+
From 398d35dc977bade3b97386124ac648d32c029884 Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Fri, 28 Nov 2025 05:48:07 -0600
Subject: [PATCH 26/27] fix: theme-color value
---
packages/desktop/index.html | 2 +-
packages/enterprise/src/entry-server.tsx | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/desktop/index.html b/packages/desktop/index.html
index 57e10defa..6e67e6d47 100644
--- a/packages/desktop/index.html
+++ b/packages/desktop/index.html
@@ -9,7 +9,7 @@
-
+
diff --git a/packages/enterprise/src/entry-server.tsx b/packages/enterprise/src/entry-server.tsx
index df095023a..85d69e2e0 100644
--- a/packages/enterprise/src/entry-server.tsx
+++ b/packages/enterprise/src/entry-server.tsx
@@ -9,6 +9,7 @@ export default createHandler(() => (
OpenCode
+
{assets}
From 0eaec2af8236c4467d85a87e1a754199a5f3a6c2 Mon Sep 17 00:00:00 2001
From: GitHub Action
Date: Fri, 28 Nov 2025 12:04:42 +0000
Subject: [PATCH 27/27] ignore: update download stats 2025-11-28
---
STATS.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/STATS.md b/STATS.md
index ab7f7413c..55a9ba565 100644
--- a/STATS.md
+++ b/STATS.md
@@ -153,3 +153,4 @@
| 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) |
+| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |