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/.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
diff --git a/STATS.md b/STATS.md
index b58a8f1c5..55a9ba565 100644
--- a/STATS.md
+++ b/STATS.md
@@ -152,3 +152,5 @@
| 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) |
+| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
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/flake.lock b/flake.lock
index b0749bea4..b67bb39a7 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1764138170,
- "narHash": "sha256-2bCmfCUZyi2yj9FFXYKwsDiaZmizN75cLhI/eWmf3tk=",
+ "lastModified": 1764230294,
+ "narHash": "sha256-Z63xl5Scj3Y/zRBPAWq1eT68n2wBWGCIEF4waZ0bQBE=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "bb813de6d2241bcb1b5af2d3059f560c66329967",
+ "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="
}
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: {
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/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx
index 60f9e9ef5..c990bf87f 100644
--- a/packages/desktop/src/pages/session.tsx
+++ b/packages/desktop/src/pages/session.tsx
@@ -333,14 +333,19 @@ export default function Page() {
flex: layout.review.state() === "pane",
}}
>
-
+
1}>
<>
-
+
New session
@@ -390,12 +399,14 @@ export default function Page() {
-
-
{
- inputRef = el
- }}
- />
+
+
+
{
+ inputRef = el
+ }}
+ />
+
@@ -498,7 +509,7 @@ export default function Page() {
-
+
{
inputRef = el
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}
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()}
+
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/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
index ba1dc70b2..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()
@@ -21,73 +22,48 @@ 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()
- 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]
@@ -137,13 +113,14 @@ 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: 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,
@@ -165,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
}),
@@ -196,7 +173,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(() =>
)
},
@@ -204,6 +181,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/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
index 30a8bb2fc..8ba7845f2 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
@@ -26,13 +26,15 @@ 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,
- 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() {
const methods = sync.data.provider_auth[provider.id] ?? [
{
@@ -85,7 +87,6 @@ export function createDialogProviderOptions() {
}
},
})),
- sortBy((x) => PROVIDER_PRIORITY[x.value] ?? 99),
)
})
return options
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
-
)
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..0b15340b2 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) {
@@ -171,8 +173,10 @@ export function DialogSelect(props: DialogSelectProps) {
}
props.ref?.(ref)
+ const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled) ?? [])
+
return (
-
+
@@ -253,18 +257,20 @@ export function DialogSelect(props: DialogSelectProps) {
)}
-
-
- {(item) => (
-
-
- {item.title}{" "}
-
- {Keybind.toString(item.keybind)}
-
- )}
-
-
+ }>
+
+
+ {(item) => (
+
+
+ {item.title}{" "}
+
+ {Keybind.toString(item.keybind)}
+
+ )}
+
+
+
)
}
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/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) {
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, {
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index 40740fa1f..1c2ba97f3 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -17,7 +17,8 @@ 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"
+import { unwrap } from "solid-js/store"
export interface MessageProps {
message: MessageType
@@ -83,15 +84,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(unwrap(props.part), props.sanitize))
return (
-
+
)
}
@@ -102,7 +98,6 @@ export interface ToolProps {
tool: string
output?: string
hideDetails?: boolean
- sanitize?: RegExp
}
export type ToolComponent = Component
@@ -170,7 +165,6 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
metadata={metadata}
output={part.state.status === "completed" ? part.state.output : undefined}
hideDetails={props.hideDetails}
- sanitize={props.sanitize}
/>
@@ -182,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 (
@@ -211,7 +205,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 +216,9 @@ ToolRegistry.register({
name: "list",
render(props) {
return (
-
+
- {sanitize(props.output, props.sanitize)}
+ {props.output}
)
@@ -335,7 +326,7 @@ ToolRegistry.register({
>
@@ -355,13 +346,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 +363,11 @@ ToolRegistry.register({
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,
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") {
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.
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 |