Compare commits

..

62 Commits

Author SHA1 Message Date
Aiden Cline 4217091d02 feat: add retry logic 2026-04-23 13:41:29 -04:00
Aiden Cline f778c685f5 test: add additional test cases 2026-04-23 12:45:16 -04:00
opencode-agent[bot] 353532b1c1 chore: generate 2026-04-23 16:00:01 +00:00
Shoubhit Dash 9df7c78ebe fix(npm): respect npmrc for version lookups (#24016) 2026-04-23 21:28:54 +05:30
opencode eb7555d3c6 sync release versions for v1.14.22 2026-04-23 15:56:49 +00:00
opencode-agent[bot] 2cd89d64e9 chore: generate 2026-04-23 15:31:16 +00:00
Kit Langton 0517ab4695 refactor(session): migrate session domain to Effect Schema (#24005) 2026-04-23 11:30:02 -04:00
James Long bbf67d0fff fix(tui): render all non-synthetic text parts of a user message (#24009) 2026-04-23 11:24:40 -04:00
Shoubhit Dash 38deb0f3ee fix(npm): respect npmrc config (#24001) 2026-04-23 19:54:01 +05:30
Simon Klee 9b6db08d21 chore: add to TEAM_MEMBERS (#23975) 2026-04-23 13:02:40 +02:00
opencode-agent[bot] 3ae74cb047 chore: generate 2026-04-23 10:58:14 +00:00
Brendan Allan 6002500bc0 feat(project): add icon_url_override field to projects (#23955) 2026-04-23 18:57:04 +08:00
Brendan Allan 785f3589ab fix: add keyed prop to Show components for proper reactivity (#23935) 2026-04-23 17:32:01 +08:00
Frank a419f1c50f zen: hy3 preview 2026-04-23 02:56:41 -04:00
opencode 871789ce64 sync release versions for v1.14.21 2026-04-23 05:45:21 +00:00
Brendan Allan df27baa00d refactor: remove redundant pending check from working memo (#23929) 2026-04-23 13:15:02 +08:00
Aiden Cline 9730008543 tweak: codex model logic (#23925) 2026-04-23 00:29:56 -04:00
Luke Parker ac26394fcb fix(beta): PR resolvers/smoke check should typecheck all pacakges (#23913) 2026-04-23 00:25:41 -04:00
Luke Parker 6387b35a2d log session sdk errors (#23652) 2026-04-23 00:25:41 -04:00
opencode-agent[bot] 1cd4c92242 chore: generate 2026-04-23 00:25:40 -04:00
Luke Parker e383df4b17 feat: support pull diagnostics in the LSP client (C#, Kotlin, etc) (#23771) 2026-04-23 00:25:40 -04:00
Caleb Norton 58db41b4b9 chore: update nix bun version (#23881) 2026-04-23 00:25:40 -04:00
opencode-agent[bot] 5d133f2785 chore: generate 2026-04-23 00:25:39 -04:00
Jack e9b1d3b940 docs: add MiMo V2.5 to Go pages (#23876) 2026-04-23 00:25:39 -04:00
Steven T. Cramer 3a082a0efd fix(project): use git common dir for bare repo project cache (#19054) 2026-04-23 00:25:39 -04:00
opencode-agent[bot] 504fd1b373 chore: generate 2026-04-23 00:25:39 -04:00
Shoubhit Dash 574b2c2170 fix(session): improve session compaction (#23870) 2026-04-23 00:25:38 -04:00
opencode-agent[bot] fa8b7bc4d2 chore: generate 2026-04-23 00:25:38 -04:00
Shoubhit Dash 6196b81e0a fix(tui): fail fast on invalid session startup (#23837) 2026-04-23 00:25:38 -04:00
Brendan Allan d884ab73d5 fix: consolidate project avatar source logic (#23819) 2026-04-23 00:25:37 -04:00
opencode-agent[bot] 71d196d3cd chore: generate 2026-04-23 00:25:37 -04:00
Luke Parker 20756e0ee4 test: fix cross-spawn stderr race on Windows CI (#23808) 2026-04-23 00:25:37 -04:00
opencode-agent[bot] 894e638914 chore: generate 2026-04-23 00:25:37 -04:00
Luke Parker 8113a4360e fix: preserve BOM in text tool round-trips (#23797) 2026-04-23 00:25:36 -04:00
opencode-agent[bot] c819804638 chore: update nix node_modules hashes 2026-04-23 00:25:36 -04:00
Brendan Allan 06066dbb7b fix(app): improve icon override handling in project edit dialog (#23768) 2026-04-23 00:25:36 -04:00
Luke Parker 69b8ea0d66 chore: bump Bun to 1.3.13 (#23791) 2026-04-23 00:25:36 -04:00
opencode-agent[bot] b0455583aa chore: generate 2026-04-22 03:41:42 +00:00
Kit Langton ed802fd121 refactor(core): migrate MessageV2 errors to Schema-backed named errors (#23764) 2026-04-21 23:40:32 -04:00
Kit Langton 1593c3ed16 refactor(core): migrate MessageV2 internal Cursor to Effect Schema (#23763) 2026-04-21 23:28:33 -04:00
Kit Langton e89543811c refactor(core): migrate MessageV2 message DTOs (User/Assistant/Part/Info/WithParts) to Effect Schema (#23757) 2026-04-21 23:26:12 -04:00
opencode-agent[bot] 1a76799fd8 chore: generate 2026-04-22 03:18:25 +00:00
Kit Langton fa623964a2 refactor(core): migrate MessageV2 part leaves + ToolPart to Effect Schema (#23756) 2026-04-21 23:17:23 -04:00
Frank 628102ad04 zen: handle alibaba format 2026-04-21 23:13:44 -04:00
Kit Langton ad7ae7353f refactor(core): derive all schema.ts leaves' .zod via effect-zod walker (#23754) 2026-04-21 22:51:18 -04:00
NN708 8043cfa68d fix(desktop): update desktop file and MetaInfo file (#14933) 2026-04-22 07:19:04 +08:00
opencode-agent[bot] d2181e9273 chore: generate 2026-04-21 22:04:16 +00:00
Mathews Bryan 5e9fb3cc90 feat: replace csharp-ls with roslyn-language-server (#14463)
Co-authored-by: Mathews <Mathews.Bryan@cincpro.com>
2026-04-21 22:03:09 +00:00
Kit Langton 2da6d860e0 refactor(core): derive provider schema .zod via effect-zod walker (#23753) 2026-04-21 17:49:24 -04:00
Kit Langton df0c1f649c refactor(core): migrate MessageV2 tool state schemas to Effect Schema (#23752) 2026-04-21 17:47:50 -04:00
Kit Langton d6dea3f3e0 chore(core): clean up after ConfigPermission Effect Schema migration (#23749) 2026-04-21 17:40:54 -04:00
Kit Langton 0bcf734a67 migrate Snapshot schemas to Effect Schema (#23747) 2026-04-21 17:37:27 -04:00
opencode-agent[bot] b1c3095edd chore: generate 2026-04-21 21:34:17 +00:00
Kit Langton b0f565b74a refactor(core): migrate ConfigPermission.Info to Effect Schema canonical (#23740) 2026-04-21 17:33:13 -04:00
Kit Langton 2ae64f426b refactor(core): migrate MessageV2.Format to Effect Schema (#23744) 2026-04-21 17:30:08 -04:00
Kit Langton 7933657135 migrate LSP data schemas to Effect Schema (#23745) 2026-04-21 17:26:50 -04:00
Frank caaddf0964 zen: ling 2.6 free 2026-04-21 17:06:31 -04:00
Ruben De Smet 1a20703469 feat: add Mistral Small reasoning variant support (issue #19479) (#23735) 2026-04-21 16:45:06 -04:00
github-actions[bot] 8751f48a75 Update VOUCHED list
https://github.com/anomalyco/opencode/issues/23735#issuecomment-4291498854
2026-04-21 20:17:13 +00:00
Aiden Cline 58232d896e fix: dont show variants for kimi models that dont support them (#23696) 2026-04-21 15:33:35 -04:00
Rahul Iyer cd6415f332 fix(tui): don't check for version upgrades if it's disabled by the user (#20089)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-21 15:30:15 -04:00
opencode c9fb8d0ce7 sync release versions for v1.14.20 2026-04-21 19:07:36 +00:00
170 changed files with 6669 additions and 2144 deletions
+1
View File
@@ -13,3 +13,4 @@ R44VC0RP
rekram1-node
RhysSullivan
thdxr
simonklee
+1
View File
@@ -27,6 +27,7 @@ r44vc0rp
rekram1-node
-ricardo-m-l
-robinmordasiewicz
rubdos
shantur
simonklee
-spider-yamet clawdbot/llm psychosis, spam pinging the team
+19 -19
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -83,7 +83,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -117,7 +117,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -144,7 +144,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.48",
@@ -168,7 +168,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -192,7 +192,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -225,7 +225,7 @@
},
"packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -269,7 +269,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"@opencode-ai/shared": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -298,7 +298,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -314,7 +314,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.14.19",
"version": "1.14.22",
"bin": {
"opencode": "./bin/opencode",
},
@@ -459,7 +459,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"effect": "catalog:",
@@ -494,7 +494,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -509,7 +509,7 @@
},
"packages/shared": {
"name": "@opencode-ai/shared",
"version": "1.14.19",
"version": "1.14.22",
"bin": {
"opencode": "./bin/opencode",
},
@@ -533,7 +533,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -568,7 +568,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -617,7 +617,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -688,7 +688,7 @@
"@tailwindcss/vite": "4.1.11",
"@tsconfig/bun": "1.0.9",
"@tsconfig/node22": "22.0.2",
"@types/bun": "1.3.11",
"@types/bun": "1.3.12",
"@types/cross-spawn": "6.0.6",
"@types/luxon": "3.7.1",
"@types/node": "22.13.9",
@@ -2302,7 +2302,7 @@
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
"@types/cacache": ["@types/cacache@20.0.1", "", { "dependencies": { "@types/node": "*", "minipass": "*" } }, "sha512-QlKW3AFoFr/hvPHwFHMIVUH/ZCYeetBNou3PCmxu5LaNDvrtBlPJtIA6uhmU9JRt9oxj7IYoqoLcpxtzpPiTcw=="],
@@ -2720,7 +2720,7 @@
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
"bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1773909469,
"narHash": "sha256-vglVrLfHjFIzIdV9A27Ugul6rh3I1qHbbitGW7dk420=",
"lastModified": 1776683584,
"narHash": "sha256-NuTLMrr10Tng72hurYG8jYQ4XKK8wnpJmOGcPiis96g=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7149c06513f335be57f26fcbbbe34afda923882b",
"rev": "9dd5558b06dbdacbf635a3dd36dce1b1a7ee3a89",
"type": "github"
},
"original": {
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-NczRp8MPppkqP8PQfWMUWJ/Wofvf2YVy5m4i22Pi3jg=",
"aarch64-linux": "sha256-QIxGOu8Fj+sWgc9hKvm1BLiIErxEtd17SPlwZGac9sQ=",
"aarch64-darwin": "sha256-Rb9qbMM+ARn0iBCaZurwcoUBCplbMXEZwrXVKextp3I=",
"x86_64-darwin": "sha256-KVxOKkaVV7W+K4reEk14MTLgmtoqwCYDqDNXNeS6ync="
"x86_64-linux": "sha256-AgHhYsiygxbsBo3JN4HqHXKAwh8n1qeuSCe2qqxlxW4=",
"aarch64-linux": "sha256-h2lpWRQ5EDYnjpqZXtUAp1mxKLQxJ4m8MspgSY8Ev78=",
"aarch64-darwin": "sha256-xnd91+WyeAqn06run2ajsekxJvTMiLsnqNPe/rR8VTM=",
"x86_64-darwin": "sha256-rXpz45IOjGEk73xhP9VY86eOj2CZBg2l1vzwzTIOOOQ="
}
}
+2 -2
View File
@@ -4,7 +4,7 @@
"description": "AI-powered development tool",
"private": true,
"type": "module",
"packageManager": "bun@1.3.11",
"packageManager": "bun@1.3.13",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"dev:desktop": "bun --cwd packages/desktop-electron dev",
@@ -30,7 +30,7 @@
"@effect/opentelemetry": "4.0.0-beta.48",
"@effect/platform-node": "4.0.0-beta.48",
"@npmcli/arborist": "9.4.0",
"@types/bun": "1.3.11",
"@types/bun": "1.3.12",
"@types/cross-spawn": "6.0.6",
"@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.14.19",
"version": "1.14.22",
"description": "",
"type": "module",
"exports": {
@@ -12,6 +12,7 @@ import { type LocalProject, getAvatarColors } from "@/context/layout"
import { getFilename } from "@opencode-ai/shared/util/path"
import { Avatar } from "@opencode-ai/ui/avatar"
import { useLanguage } from "@/context/language"
import { getProjectAvatarSource } from "@/pages/layout/sidebar-items"
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
@@ -26,8 +27,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
const [store, setStore] = createStore({
name: defaultName(),
color: props.project.icon?.color || "pink",
iconUrl: props.project.icon?.override || "",
color: props.project.icon?.color,
iconOverride: props.project.icon?.override,
startup: props.project.commands?.start ?? "",
dragOver: false,
iconHover: false,
@@ -39,7 +40,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
if (!file.type.startsWith("image/")) return
const reader = new FileReader()
reader.onload = (e) => {
setStore("iconUrl", e.target?.result as string)
setStore("iconOverride", e.target?.result as string)
setStore("iconHover", false)
}
reader.readAsDataURL(file)
@@ -68,7 +69,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
}
function clearIcon() {
setStore("iconUrl", "")
setStore("iconOverride", "")
}
const saveMutation = useMutation(() => ({
@@ -81,17 +82,17 @@ export function DialogEditProject(props: { project: LocalProject }) {
projectID: props.project.id,
directory: props.project.worktree,
name,
icon: { color: store.color, override: store.iconUrl },
icon: { color: store.color || "", override: store.iconOverride || "" },
commands: { start },
})
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
globalSync.project.icon(props.project.worktree, store.iconOverride || undefined)
dialog.close()
return
}
globalSync.project.meta(props.project.worktree, {
name,
icon: { color: store.color, override: store.iconUrl || undefined },
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
commands: { start: start || undefined },
})
dialog.close()
@@ -130,13 +131,13 @@ export function DialogEditProject(props: { project: LocalProject }) {
classList={{
"border-text-interactive-base bg-surface-info-base/20": store.dragOver,
"border-border-base hover:border-border-strong": !store.dragOver,
"overflow-hidden": !!store.iconUrl,
"overflow-hidden": !!store.iconOverride,
}}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={() => {
if (store.iconUrl && store.iconHover) {
if (store.iconOverride && store.iconHover) {
clearIcon()
} else {
iconInput?.click()
@@ -144,7 +145,11 @@ export function DialogEditProject(props: { project: LocalProject }) {
}}
>
<Show
when={store.iconUrl}
when={getProjectAvatarSource(props.project.id, {
color: store.color,
url: props.project.icon?.url,
override: store.iconOverride,
})}
fallback={
<div class="size-full flex items-center justify-center">
<Avatar
@@ -155,18 +160,20 @@ export function DialogEditProject(props: { project: LocalProject }) {
</div>
}
>
<img
src={store.iconUrl}
alt={language.t("dialog.project.edit.icon.alt")}
class="size-full object-cover"
/>
{(src) => (
<img
src={src()}
alt={language.t("dialog.project.edit.icon.alt")}
class="size-full object-cover"
/>
)}
</Show>
</div>
<div
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
classList={{
"opacity-100": store.iconHover && !store.iconUrl,
"opacity-0": !(store.iconHover && !store.iconUrl),
"opacity-100": store.iconHover && !store.iconOverride,
"opacity-0": !(store.iconHover && !store.iconOverride),
}}
>
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
@@ -174,8 +181,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
<div
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
classList={{
"opacity-100": store.iconHover && !!store.iconUrl,
"opacity-0": !(store.iconHover && !!store.iconUrl),
"opacity-100": store.iconHover && !!store.iconOverride,
"opacity-0": !(store.iconHover && !!store.iconOverride),
}}
>
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
@@ -198,7 +205,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
</div>
</div>
<Show when={!store.iconUrl}>
<Show when={!store.iconOverride}>
<div class="flex flex-col gap-2">
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
<div class="flex gap-1.5">
@@ -215,7 +222,10 @@ export function DialogEditProject(props: { project: LocalProject }) {
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
store.color !== color,
}}
onClick={() => setStore("color", color)}
onClick={() => {
if (store.color === color && !props.project.icon?.url) return
setStore("color", store.color === color ? undefined : color)
}}
>
<Avatar
fallback={store.name || defaultName()}
+15 -33
View File
@@ -10,9 +10,8 @@ import type {
import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/shared/util/path"
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
import { createStore, produce, reconcile, unwrap } from "solid-js/store"
import { createStore, produce, reconcile } from "solid-js/store"
import { useLanguage } from "@/context/language"
import { Persist, persisted } from "@/utils/persist"
import type { InitError } from "../pages/error"
import { useGlobalSDK } from "./global-sdk"
import { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap"
@@ -24,7 +23,6 @@ import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global
import { trimSessions } from "./global-sync/session-trim"
import type { ProjectMeta } from "./global-sync/types"
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
import { sanitizeProject } from "./global-sync/utils"
import { formatServerError } from "@/utils/server-errors"
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
@@ -56,15 +54,10 @@ function createGlobalSync() {
const sessionLoads = new Map<string, Promise<void>>()
const sessionMeta = new Map<string, { limit: number }>()
const [projectCache, setProjectCache, projectInit] = persisted(
Persist.global("globalSync.project", ["globalSync.project.v1"]),
createStore({ value: [] as Project[] }),
)
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
ready: false,
path: { state: "", config: "", worktree: "", directory: "", home: "" },
project: projectCache.value,
project: [],
session_todo: {},
provider: { all: [], connected: [], default: {} },
provider_auth: {},
@@ -73,32 +66,18 @@ function createGlobalSync() {
})
const queryClient = useQueryClient()
let active = true
let projectWritten = false
let bootedAt = 0
let bootingRoot = false
let eventFrame: number | undefined
let eventTimer: ReturnType<typeof setTimeout> | undefined
onCleanup(() => {
active = false
})
onCleanup(() => {
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
if (eventTimer !== undefined) clearTimeout(eventTimer)
})
const cacheProjects = () => {
setProjectCache(
"value",
untrack(() => globalStore.project.map(sanitizeProject)),
)
}
const setProjects = (next: Project[] | ((draft: Project[]) => Project[])) => {
projectWritten = true
setGlobalStore("project", next)
cacheProjects()
}
const setBootStore = ((...input: unknown[]) => {
@@ -117,16 +96,6 @@ function createGlobalSync() {
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
}) as typeof setGlobalStore
if (projectInit instanceof Promise) {
void projectInit.then(() => {
if (!active) return
if (projectWritten) return
const cached = projectCache.value
if (cached.length === 0) return
setGlobalStore("project", cached)
})
}
const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => {
if (!sessionID) return
if (!todos) {
@@ -295,6 +264,19 @@ function createGlobalSync() {
const event = e.details
const recent = bootingRoot || Date.now() - bootedAt < 1500
if (event.type === "session.error") {
const error = event.properties.error
if (error?.name !== "MessageAbortedError") {
console.error("[global-sync] session error", {
scope: directory === "global" ? "global" : "workspace",
directory: directory === "global" ? undefined : directory,
project: directory === "global" ? undefined : getFilename(directory),
sessionID: event.properties.sessionID,
error,
})
}
}
if (directory === "global") {
applyGlobalEvent({
event,
@@ -156,13 +156,12 @@ export function createChildStoreManager(input: {
const init = () =>
createRoot((dispose) => {
const initialMeta = meta[0].value
const initialIcon = icon[0].value
const pathQuery = useQuery(() => loadPathQuery(directory))
const child = createStore<State>({
project: "",
projectMeta: initialMeta,
projectMeta: undefined,
icon: initialIcon,
provider_ready: false,
provider: { all: [], connected: [], default: {} },
@@ -208,11 +207,6 @@ export function createChildStoreManager(input: {
child[1]("vcs", (value) => value ?? cached)
})
onPersistedInit(meta[2], () => {
if (child[0].projectMeta !== initialMeta) return
child[1]("projectMeta", meta[0].value)
})
onPersistedInit(icon[2], () => {
if (child[0].icon !== initialIcon) return
child[1]("icon", icon[0].value)
+2 -32
View File
@@ -391,37 +391,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
? globalSync.data.project.find((x) => x.id === projectID)
: globalSync.data.project.find((x) => x.worktree === project.worktree)
const local = childStore.projectMeta
const localOverride =
local?.name !== undefined ||
local?.commands?.start !== undefined ||
local?.icon?.override !== undefined ||
local?.icon?.color !== undefined
const base = {
...metadata,
...project,
icon: {
url: metadata?.icon?.url,
override: metadata?.icon?.override ?? childStore.icon,
color: metadata?.icon?.color,
},
}
const isGlobal = projectID === "global" || (metadata?.id === undefined && localOverride)
if (!isGlobal) return base
return {
...base,
id: base.id ?? "global",
name: local?.name,
commands: local?.commands,
icon: {
url: base.icon?.url,
override: local?.icon?.override,
color: local?.icon?.color,
},
}
return { ...metadata, ...project }
}
const roots = createMemo(() => {
@@ -516,7 +486,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}
for (const project of projects) {
if (project.icon?.color) continue
if (project.icon?.color || project.icon?.override || project.icon?.url) continue
const worktree = project.worktree
const existing = colors[worktree]
const color = existing ?? pickAvailableColor(used)
@@ -19,6 +19,12 @@ import { childSessionOnPath, hasProjectPermissions } from "./helpers"
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
export function getProjectAvatarSource(id?: string, icon?: { color?: string; url?: string; override?: string }) {
return id === OPENCODE_PROJECT_ID
? "https://opencode.ai/favicon.svg"
: (icon?.override ?? (icon?.color ? undefined : icon?.url))
}
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
const globalSync = useGlobalSync()
const notification = useNotification()
@@ -42,11 +48,7 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
<div class="size-full rounded overflow-clip">
<Avatar
fallback={name()}
src={
props.project.id === OPENCODE_PROJECT_ID
? "https://opencode.ai/favicon.svg"
: props.project.icon?.override || props.project.icon?.url
}
src={getProjectAvatarSource(props.project.id, props.project.icon)}
{...getAvatarColors(props.project.icon?.color)}
class="size-full rounded"
classList={{ "badge-mask": notify() }}
@@ -265,10 +267,10 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
</Show>
</div>
</div>
<Show when={currentChild()}>
<Show when={currentChild()} keyed>
{(child) => (
<div class="w-full">
<SessionItem {...props} session={child()} level={(props.level ?? 0) + 1} />
<SessionItem {...props} session={child} level={(props.level ?? 0) + 1} />
</div>
)}
</Show>
@@ -259,7 +259,7 @@ export function MessageTimeline(props: {
if (!id) return idle
return sync.data.session_status[id] ?? idle
})
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
const working = createMemo(() => sessionStatus().type !== "idle")
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
const [timeoutDone, setTimeoutDone] = createSignal(true)
@@ -812,7 +812,7 @@ export function MessageTimeline(props: {
</Show>
</div>
</div>
<Show when={sessionID()}>
<Show when={sessionID()} keyed>
{(id) => (
<div class="shrink-0 flex items-center gap-3">
<SessionContextUsage placement="bottom" />
@@ -878,12 +878,12 @@ export function MessageTimeline(props: {
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</Show>
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
<DropdownMenu.Item onSelect={() => void archiveSession(id)}>
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}
>
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.14.19",
"version": "1.14.22",
"type": "module",
"license": "MIT",
"scripts": {
+4 -4
View File
@@ -249,7 +249,7 @@ export const dict = {
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
"go.meta.description":
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7.",
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7.",
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
"go.hero.body":
"يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.",
@@ -300,7 +300,7 @@ export const dict = {
"go.problem.item2": "حدود سخية ووصول موثوق",
"go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين",
"go.problem.item4":
"يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7",
"يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7",
"go.how.title": "كيف يعمل Go",
"go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.",
"go.how.step1.title": "أنشئ حسابًا",
@@ -324,7 +324,7 @@ export const dict = {
"go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.",
"go.faq.q3": "هل Go هو نفسه Zen؟",
"go.faq.a3":
"لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7.",
"لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7.",
"go.faq.q4": "كم تكلفة Go؟",
"go.faq.a4.p1.beforePricing": "تكلفة Go",
"go.faq.a4.p1.pricingLink": "$5 للشهر الأول",
@@ -347,7 +347,7 @@ export const dict = {
"go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟",
"go.faq.a9":
"تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).",
"تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).",
"zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.",
"zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم",
+4 -4
View File
@@ -253,7 +253,7 @@ export const dict = {
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
"go.meta.description":
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.",
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.",
"go.hero.title": "Modelos de codificação de baixo custo para todos",
"go.hero.body":
"O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.",
@@ -305,7 +305,7 @@ export const dict = {
"go.problem.item2": "Limites generosos e acesso confiável",
"go.problem.item3": "Feito para o maior número possível de programadores",
"go.problem.item4":
"Inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7",
"Inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7",
"go.how.title": "Como o Go funciona",
"go.how.body":
"O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.",
@@ -331,7 +331,7 @@ export const dict = {
"go.faq.a2": "O Go inclui os modelos listados abaixo, com limites generosos e acesso confiável.",
"go.faq.q3": "O Go é o mesmo que o Zen?",
"go.faq.a3":
"Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.",
"Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.",
"go.faq.q4": "Quanto custa o Go?",
"go.faq.a4.p1.beforePricing": "O Go custa",
"go.faq.a4.p1.pricingLink": "$5 no primeiro mês",
@@ -355,7 +355,7 @@ export const dict = {
"go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?",
"go.faq.a9":
"Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).",
"Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).",
"zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.",
"zen.api.error.modelNotSupported": "Modelo {{model}} não suportado",
+4 -4
View File
@@ -251,7 +251,7 @@ export const dict = {
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
"go.meta.description":
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.",
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.",
"go.hero.title": "Kodningsmodeller til lav pris for alle",
"go.hero.body":
"Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.",
@@ -302,7 +302,7 @@ export const dict = {
"go.problem.item2": "Generøse grænser og pålidelig adgang",
"go.problem.item3": "Bygget til så mange programmører som muligt",
"go.problem.item4":
"Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7",
"Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7",
"go.how.title": "Hvordan Go virker",
"go.how.body":
"Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.",
@@ -328,7 +328,7 @@ export const dict = {
"go.faq.a2": "Go inkluderer modellerne nedenfor med generøse grænser og pålidelig adgang.",
"go.faq.q3": "Er Go det samme som Zen?",
"go.faq.a3":
"Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.",
"Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.",
"go.faq.q4": "Hvad koster Go?",
"go.faq.a4.p1.beforePricing": "Go koster",
"go.faq.a4.p1.pricingLink": "$5 første måned",
@@ -351,7 +351,7 @@ export const dict = {
"go.faq.q9": "Hvad er forskellen på gratis modeller og Go?",
"go.faq.a9":
"Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).",
"Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).",
"zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.",
"zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke",
+4 -4
View File
@@ -253,7 +253,7 @@ export const dict = {
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
"go.meta.description":
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7.",
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7.",
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
"go.hero.body":
"Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.",
@@ -304,7 +304,7 @@ export const dict = {
"go.problem.item2": "Großzügige Limits und zuverlässiger Zugang",
"go.problem.item3": "Für so viele Programmierer wie möglich gebaut",
"go.problem.item4":
"Beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7",
"Beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7",
"go.how.title": "Wie Go funktioniert",
"go.how.body":
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.",
@@ -330,7 +330,7 @@ export const dict = {
"go.faq.a2": "Go umfasst die unten aufgeführten Modelle mit großzügigen Limits und zuverlässigem Zugriff.",
"go.faq.q3": "Ist Go dasselbe wie Zen?",
"go.faq.a3":
"Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7.",
"Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7.",
"go.faq.q4": "Wie viel kostet Go?",
"go.faq.a4.p1.beforePricing": "Go kostet",
"go.faq.a4.p1.pricingLink": "$5 im ersten Monat",
@@ -354,7 +354,7 @@ export const dict = {
"go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?",
"go.faq.a9":
"Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7 mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).",
"Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7 mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).",
"zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.",
"zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt",
+4 -4
View File
@@ -248,7 +248,7 @@ export const dict = {
"go.title": "OpenCode Go | Low cost coding models for everyone",
"go.meta.description":
"Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7.",
"Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7.",
"go.banner.badge": "3x",
"go.banner.text": "Kimi K2.6 gets 3× usage limits through April 27",
"go.hero.title": "Low cost coding models for everyone",
@@ -298,7 +298,7 @@ export const dict = {
"go.problem.item2": "Generous limits and reliable access",
"go.problem.item3": "Built for as many programmers as possible",
"go.problem.item4":
"Includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7",
"Includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7",
"go.how.title": "How Go works",
"go.how.body": "Go starts at $5 for your first month, then $10/month. You can use it with OpenCode or any agent.",
"go.how.step1.title": "Create an account",
@@ -323,7 +323,7 @@ export const dict = {
"go.faq.a2": "Go includes the models listed below, with generous limits and reliable access.",
"go.faq.q3": "Is Go the same as Zen?",
"go.faq.a3":
"No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7.",
"No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7.",
"go.faq.q4": "How much does Go cost?",
"go.faq.a4.p1.beforePricing": "Go costs",
"go.faq.a4.p1.pricingLink": "$5 first month",
@@ -347,7 +347,7 @@ export const dict = {
"go.faq.q9": "What is the difference between free models and Go?",
"go.faq.a9":
"Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7 with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).",
"Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7 with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).",
"zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.",
"zen.api.error.modelNotSupported": "Model {{model}} not supported",
+4 -4
View File
@@ -254,7 +254,7 @@ export const dict = {
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
"go.meta.description":
"Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7.",
"Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7.",
"go.hero.title": "Modelos de programación de bajo coste para todos",
"go.hero.body":
"Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.",
@@ -306,7 +306,7 @@ export const dict = {
"go.problem.item2": "Límites generosos y acceso fiable",
"go.problem.item3": "Creado para tantos programadores como sea posible",
"go.problem.item4":
"Incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7",
"Incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7",
"go.how.title": "Cómo funciona Go",
"go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.",
"go.how.step1.title": "Crear una cuenta",
@@ -331,7 +331,7 @@ export const dict = {
"go.faq.a2": "Go incluye los modelos que se indican abajo, con límites generosos y acceso confiable.",
"go.faq.q3": "¿Es Go lo mismo que Zen?",
"go.faq.a3":
"No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7.",
"No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7.",
"go.faq.q4": "¿Cuánto cuesta Go?",
"go.faq.a4.p1.beforePricing": "Go cuesta",
"go.faq.a4.p1.pricingLink": "$5 el primer mes",
@@ -355,7 +355,7 @@ export const dict = {
"go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?",
"go.faq.a9":
"Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7 con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).",
"Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7 con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).",
"zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.",
"zen.api.error.modelNotSupported": "Modelo {{model}} no soportado",
+4 -4
View File
@@ -255,7 +255,7 @@ export const dict = {
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
"go.meta.description":
"Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7.",
"Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7.",
"go.hero.title": "Modèles de code à faible coût pour tous",
"go.hero.body":
"Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.",
@@ -306,7 +306,7 @@ export const dict = {
"go.problem.item2": "Limites généreuses et accès fiable",
"go.problem.item3": "Conçu pour autant de programmeurs que possible",
"go.problem.item4":
"Inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7",
"Inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7",
"go.how.title": "Comment fonctionne Go",
"go.how.body":
"Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.",
@@ -332,7 +332,7 @@ export const dict = {
"go.faq.a2": "Go inclut les modèles ci-dessous, avec des limites généreuses et un accès fiable.",
"go.faq.q3": "Est-ce que Go est la même chose que Zen ?",
"go.faq.a3":
"Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7.",
"Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7.",
"go.faq.q4": "Combien coûte Go ?",
"go.faq.a4.p1.beforePricing": "Go coûte",
"go.faq.a4.p1.pricingLink": "$5 le premier mois",
@@ -355,7 +355,7 @@ export const dict = {
"Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.",
"go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?",
"go.faq.a9":
"Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7 avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).",
"Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7 avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).",
"zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.",
"zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge",
+4 -4
View File
@@ -251,7 +251,7 @@ export const dict = {
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
"go.meta.description":
"Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.",
"Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.",
"go.hero.title": "Modelli di coding a basso costo per tutti",
"go.hero.body":
"Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.",
@@ -302,7 +302,7 @@ export const dict = {
"go.problem.item2": "Limiti generosi e accesso affidabile",
"go.problem.item3": "Costruito per il maggior numero possibile di programmatori",
"go.problem.item4":
"Include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7",
"Include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7",
"go.how.title": "Come funziona Go",
"go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.",
"go.how.step1.title": "Crea un account",
@@ -327,7 +327,7 @@ export const dict = {
"go.faq.a2": "Go include i modelli elencati di seguito, con limiti generosi e accesso affidabile.",
"go.faq.q3": "Go è lo stesso di Zen?",
"go.faq.a3":
"No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.",
"No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.",
"go.faq.q4": "Quanto costa Go?",
"go.faq.a4.p1.beforePricing": "Go costa",
"go.faq.a4.p1.pricingLink": "$5 il primo mese",
@@ -351,7 +351,7 @@ export const dict = {
"go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?",
"go.faq.a9":
"I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7 con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).",
"I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7 con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).",
"zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.",
"zen.api.error.modelNotSupported": "Modello {{model}} non supportato",
+4 -4
View File
@@ -250,7 +250,7 @@ export const dict = {
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
"go.meta.description":
"Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7に対して5時間のゆとりあるリクエスト上限があります。",
"Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7に対して5時間のゆとりあるリクエスト上限があります。",
"go.hero.title": "すべての人のための低価格なコーディングモデル",
"go.hero.body":
"Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。",
@@ -302,7 +302,7 @@ export const dict = {
"go.problem.item2": "十分な制限と安定したアクセス",
"go.problem.item3": "できるだけ多くのプログラマーのために構築",
"go.problem.item4":
"GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7を含む",
"GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7を含む",
"go.how.title": "Goの仕組み",
"go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。",
"go.how.step1.title": "アカウントを作成",
@@ -327,7 +327,7 @@ export const dict = {
"go.faq.a2": "Go には、十分な利用上限と安定したアクセスを備えた、以下のモデルが含まれます。",
"go.faq.q3": "GoはZenと同じですか?",
"go.faq.a3":
"いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7のオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。",
"いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7のオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。",
"go.faq.q4": "Goの料金は?",
"go.faq.a4.p1.beforePricing": "Goは",
"go.faq.a4.p1.pricingLink": "最初の月$5",
@@ -351,7 +351,7 @@ export const dict = {
"go.faq.q9": "無料モデルとGoの違いは何ですか?",
"go.faq.a9":
"無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7が含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。",
"無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7が含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。",
"zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。",
"zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません",
+4 -4
View File
@@ -247,7 +247,7 @@ export const dict = {
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
"go.meta.description":
"Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7에 대해 넉넉한 5시간 요청 한도를 제공합니다.",
"Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7에 대해 넉넉한 5시간 요청 한도를 제공합니다.",
"go.hero.title": "모두를 위한 저비용 코딩 모델",
"go.hero.body":
"Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.",
@@ -299,7 +299,7 @@ export const dict = {
"go.problem.item2": "넉넉한 한도와 안정적인 액세스",
"go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨",
"go.problem.item4":
"GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7 포함",
"GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7 포함",
"go.how.title": "Go 작동 방식",
"go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.",
"go.how.step1.title": "계정 생성",
@@ -323,7 +323,7 @@ export const dict = {
"go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 아래 모델이 포함됩니다.",
"go.faq.q3": "Go는 Zen과 같은가요?",
"go.faq.a3":
"아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.",
"아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.",
"go.faq.q4": "Go 비용은 얼마인가요?",
"go.faq.a4.p1.beforePricing": "Go 비용은",
"go.faq.a4.p1.pricingLink": "첫 달 $5",
@@ -346,7 +346,7 @@ export const dict = {
"go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?",
"go.faq.a9":
"무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).",
"무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).",
"zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.",
"zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다",
+4 -4
View File
@@ -251,7 +251,7 @@ export const dict = {
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
"go.meta.description":
"Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.",
"Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.",
"go.hero.title": "Rimelige kodemodeller for alle",
"go.hero.body":
"Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.",
@@ -302,7 +302,7 @@ export const dict = {
"go.problem.item2": "Rause grenser og pålitelig tilgang",
"go.problem.item3": "Bygget for så mange programmerere som mulig",
"go.problem.item4":
"Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7",
"Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7",
"go.how.title": "Hvordan Go fungerer",
"go.how.body":
"Go starter på $5 for den første måneden, deretter $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.",
@@ -328,7 +328,7 @@ export const dict = {
"go.faq.a2": "Go inkluderer modellene nedenfor, med høye grenser og pålitelig tilgang.",
"go.faq.q3": "Er Go det samme som Zen?",
"go.faq.a3":
"Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.",
"Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.",
"go.faq.q4": "Hva koster Go?",
"go.faq.a4.p1.beforePricing": "Go koster",
"go.faq.a4.p1.pricingLink": "$5 første måned",
@@ -352,7 +352,7 @@ export const dict = {
"go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?",
"go.faq.a9":
"Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7 med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).",
"Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7 med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).",
"zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.",
"zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke",
+4 -4
View File
@@ -252,7 +252,7 @@ export const dict = {
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
"go.meta.description":
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7.",
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7.",
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
"go.hero.body":
"Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.",
@@ -303,7 +303,7 @@ export const dict = {
"go.problem.item2": "Hojne limity i niezawodny dostęp",
"go.problem.item3": "Stworzony dla jak największej liczby programistów",
"go.problem.item4":
"Zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7",
"Zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7",
"go.how.title": "Jak działa Go",
"go.how.body":
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.",
@@ -329,7 +329,7 @@ export const dict = {
"go.faq.a2": "Go obejmuje poniższe modele z wysokimi limitami i niezawodnym dostępem.",
"go.faq.q3": "Czy Go to to samo co Zen?",
"go.faq.a3":
"Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7.",
"Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7.",
"go.faq.q4": "Ile kosztuje Go?",
"go.faq.a4.p1.beforePricing": "Go kosztuje",
"go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc",
@@ -353,7 +353,7 @@ export const dict = {
"go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?",
"go.faq.a9":
"Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7 z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).",
"Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7 z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).",
"zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.",
"zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany",
+4 -4
View File
@@ -255,7 +255,7 @@ export const dict = {
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
"go.meta.description":
"Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7.",
"Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7.",
"go.hero.title": "Недорогие модели для кодинга для всех",
"go.hero.body":
"Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.",
@@ -307,7 +307,7 @@ export const dict = {
"go.problem.item2": "Щедрые лимиты и надежный доступ",
"go.problem.item3": "Создан для максимального числа программистов",
"go.problem.item4":
"Включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7",
"Включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7",
"go.how.title": "Как работает Go",
"go.how.body":
"Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.",
@@ -333,7 +333,7 @@ export const dict = {
"go.faq.a2": "Go включает перечисленные ниже модели с щедрыми лимитами и надежным доступом.",
"go.faq.q3": "Go — это то же самое, что и Zen?",
"go.faq.a3":
"Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7.",
"Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7.",
"go.faq.q4": "Сколько стоит Go?",
"go.faq.a4.p1.beforePricing": "Go стоит",
"go.faq.a4.p1.pricingLink": "$5 за первый месяц",
@@ -357,7 +357,7 @@ export const dict = {
"go.faq.q9": "В чем разница между бесплатными моделями и Go?",
"go.faq.a9":
"Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7 с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).",
"Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7 с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).",
"zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.",
"zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается",
+4 -4
View File
@@ -250,7 +250,7 @@ export const dict = {
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
"go.meta.description":
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7",
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7",
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
"go.hero.body":
"Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน",
@@ -300,7 +300,7 @@ export const dict = {
"go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้",
"go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้",
"go.problem.item4":
"รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7",
"รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7",
"go.how.title": "Go ทำงานอย่างไร",
"go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้",
"go.how.step1.title": "สร้างบัญชี",
@@ -325,7 +325,7 @@ export const dict = {
"go.faq.a2": "Go รวมโมเดลด้านล่างนี้ พร้อมขีดจำกัดที่มากและการเข้าถึงที่เชื่อถือได้",
"go.faq.q3": "Go เหมือนกับ Zen หรือไม่?",
"go.faq.a3":
"ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7 อย่างเชื่อถือได้",
"ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7 อย่างเชื่อถือได้",
"go.faq.q4": "Go ราคาเท่าไหร่?",
"go.faq.a4.p1.beforePricing": "Go ราคา",
"go.faq.a4.p1.pricingLink": "$5 เดือนแรก",
@@ -348,7 +348,7 @@ export const dict = {
"go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?",
"go.faq.a9":
"โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7 ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)",
"โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7 ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)",
"zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง",
"zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}",
+4 -4
View File
@@ -253,7 +253,7 @@ export const dict = {
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
"go.meta.description":
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 için cömert 5 saatlik istek limitleri sunar.",
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 için cömert 5 saatlik istek limitleri sunar.",
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
"go.hero.body":
"Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.",
@@ -305,7 +305,7 @@ export const dict = {
"go.problem.item2": "Cömert limitler ve güvenilir erişim",
"go.problem.item3": "Mümkün olduğunca çok programcı için geliştirildi",
"go.problem.item4":
"GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 içerir",
"GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 içerir",
"go.how.title": "Go nasıl çalışır?",
"go.how.body":
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.",
@@ -331,7 +331,7 @@ export const dict = {
"go.faq.a2": "Go, aşağıda listelenen modelleri cömert limitler ve güvenilir erişimle sunar.",
"go.faq.q3": "Go, Zen ile aynı mı?",
"go.faq.a3":
"Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.",
"Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.",
"go.faq.q4": "Go ne kadar?",
"go.faq.a4.p1.beforePricing": "Go'nun maliyeti",
"go.faq.a4.p1.pricingLink": "İlk ay $5",
@@ -355,7 +355,7 @@ export const dict = {
"go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?",
"go.faq.a9":
"Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).",
"Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).",
"zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.",
"zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor",
+4 -4
View File
@@ -241,7 +241,7 @@ export const dict = {
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
"go.meta.description":
"Go 首月 $5,之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 的 5 小时充裕请求额度。",
"Go 首月 $5,之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 的 5 小时充裕请求额度。",
"go.hero.title": "人人可用的低成本编程模型",
"go.hero.body":
"Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。",
@@ -291,7 +291,7 @@ export const dict = {
"go.problem.item2": "充裕的限额和可靠的访问",
"go.problem.item3": "为尽可能多的程序员打造",
"go.problem.item4":
"包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 和 MiniMax M2.7",
"包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 和 MiniMax M2.7",
"go.how.title": "Go 如何工作",
"go.how.body": "Go 起价为首月 $5,之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。",
"go.how.step1.title": "创建账户",
@@ -313,7 +313,7 @@ export const dict = {
"go.faq.a2": "Go 包含下方列出的模型,提供充足的限额和可靠的访问。",
"go.faq.q3": "Go 和 Zen 一样吗?",
"go.faq.a3":
"不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 等开源模型。",
"不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 等开源模型。",
"go.faq.q4": "Go 多少钱?",
"go.faq.a4.p1.beforePricing": "Go 费用为",
"go.faq.a4.p1.pricingLink": "首月 $5",
@@ -335,7 +335,7 @@ export const dict = {
"go.faq.q9": "免费模型和 Go 之间的区别是什么?",
"go.faq.a9":
"免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 和 MiniMax M2.7,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。",
"免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 和 MiniMax M2.7,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。",
"zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。",
"zen.api.error.modelNotSupported": "不支持模型 {{model}}",
+4 -4
View File
@@ -241,7 +241,7 @@ export const dict = {
"go.title": "OpenCode Go | 低成本全民編碼模型",
"go.meta.description":
"Go 首月 $5,之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 的 5 小時充裕請求額度。",
"Go 首月 $5,之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 的 5 小時充裕請求額度。",
"go.hero.title": "低成本全民編碼模型",
"go.hero.body":
"Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。",
@@ -291,7 +291,7 @@ export const dict = {
"go.problem.item2": "寬裕的限額與穩定存取",
"go.problem.item3": "專為盡可能多的程式設計師打造",
"go.problem.item4":
"包含 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 與 MiniMax M2.7",
"包含 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 與 MiniMax M2.7",
"go.how.title": "Go 如何運作",
"go.how.body": "Go 起價為首月 $5,之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。",
"go.how.step1.title": "建立帳號",
@@ -313,7 +313,7 @@ export const dict = {
"go.faq.a2": "Go 包含下方列出的模型,提供充足的額度與穩定的存取。",
"go.faq.q3": "Go 與 Zen 一樣嗎?",
"go.faq.a3":
"不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 等開源模型。",
"不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 等開源模型。",
"go.faq.q4": "Go 費用是多少?",
"go.faq.a4.p1.beforePricing": "Go 費用為",
"go.faq.a4.p1.pricingLink": "首月 $5",
@@ -335,7 +335,7 @@ export const dict = {
"go.faq.q9": "免費模型與 Go 有什麼區別?",
"go.faq.a9":
"免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 與 MiniMax M2.7,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。",
"免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 與 MiniMax M2.7,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。",
"zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。",
"zen.api.error.modelNotSupported": "不支援模型 {{model}}",
+3 -1
View File
@@ -29,6 +29,8 @@ const models = [
{ name: "Kimi K2.6", provider: "Moonshot AI" },
{ name: "MiMo-V2-Pro", provider: "Xiaomi MiMo" },
{ name: "MiMo-V2-Omni", provider: "Xiaomi MiMo" },
{ name: "MiMo-V2.5-Pro", provider: "Xiaomi MiMo" },
{ name: "MiMo-V2.5", provider: "Xiaomi MiMo" },
{ name: "Qwen3.5 Plus", provider: "Alibaba Cloud Model Studio" },
{ name: "Qwen3.6 Plus", provider: "Alibaba Cloud Model Studio" },
{ name: "MiniMax M2.7", provider: "MiniMax" },
@@ -60,7 +62,7 @@ function LimitsGraph(props: { href: string }) {
const graph = [
{ id: "glm-5.1", name: "GLM-5.1", req: 880, d: "100ms" },
{ id: "kimi-k2.6", name: "Kimi K2.6 (3x usage)", req: 3450, baseReq: 1150, d: "150ms" },
{ id: "mimo-v2-pro", name: "MiMo-V2-Pro", req: 1290, d: "150ms" },
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 1290, d: "150ms" },
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", req: 3300, d: "280ms" },
{ id: "minimax-m2.7", name: "MiniMax M2.7", req: 3400, d: "300ms" },
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus", req: 10200, d: "360ms" },
@@ -289,8 +289,10 @@ export function LiteSection() {
<li>Kimi K2.6</li>
<li>GLM-5</li>
<li>GLM-5.1</li>
<li>Mimo-V2-Pro</li>
<li>Mimo-V2-Omni</li>
<li>MiMo-V2-Pro</li>
<li>MiMo-V2-Omni</li>
<li>MiMo-V2.5-Pro</li>
<li>MiMo-V2.5</li>
<li>MiniMax M2.5</li>
<li>MiniMax M2.7</li>
<li>Qwen3.5 Plus</li>
@@ -148,11 +148,13 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
return {
parse: (chunk: string) => {
const data = chunk.split("\n")[1]
if (!data.startsWith("data: ")) return
// Claude models start with "data: {"
// Alibaba models start with "data:{"
if (!data.startsWith("data:")) return
let json
try {
json = JSON.parse(data.slice(6))
json = JSON.parse(data.replace(/^data:\s*/, ""))
} catch {
return
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.14.19",
"version": "1.14.22",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.14.19",
"version": "1.14.22",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.14.19",
"version": "1.14.22",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -4,7 +4,7 @@ FROM ${REGISTRY}/build/base:24.04
SHELL ["/bin/bash", "-lc"]
ARG NODE_VERSION=24.4.0
ARG BUN_VERSION=1.3.11
ARG BUN_VERSION=1.3.13
ENV BUN_INSTALL=/opt/bun
ENV PATH=/opt/bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop-electron",
"private": true,
"version": "1.14.19",
"version": "1.14.22",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.14.19",
"version": "1.14.22",
"type": "module",
"license": "MIT",
"scripts": {
@@ -28,11 +28,14 @@
<screenshots>
<screenshot type="default">
<image>https://opencode.ai/docs/_astro/screenshot.Bs5D4atL_ZvsvFu.webp</image>
<image>https://raw.githubusercontent.com/anomalyco/opencode/b75d4d1c5ec449585d515c756fc81f080a157a9a/packages/web/src/assets/lander/screenshot.png</image>
</screenshot>
</screenshots>
<releases>
<release version="1.4.0" date="2026-04-08">
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.4.0</url>
</release>
<release version="1.0.223" date="2026-01-01">
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.223</url>
</release>
@@ -32,6 +32,7 @@
"icons/dev/icon.ico"
],
"active": true,
"category": "DeveloperTool",
"targets": ["deb", "rpm", "dmg", "nsis", "app"],
"externalBin": ["sidecars/opencode-cli"],
"linux": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.14.19",
"version": "1.14.22",
"private": true,
"type": "module",
"license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.14.19"
version = "1.14.22"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.22/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.22/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.22/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.22/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.19/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.22/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.14.19",
"version": "1.14.22",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
@@ -0,0 +1,2 @@
ALTER TABLE `project` ADD `icon_url_override` text;
UPDATE `project` SET `icon_url_override` = `icon_url` WHERE `icon_url` IS NOT NULL;
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.19",
"version": "1.14.22",
"name": "opencode",
"type": "module",
"license": "MIT",
+95 -18
View File
@@ -159,30 +159,107 @@ Schema at source.
These are the highest-priority next targets. Each is a small, self-contained
schema module with a clear domain.
- [ ] `src/control-plane/schema.ts`
- [ ] `src/permission/schema.ts`
- [ ] `src/project/schema.ts`
- [ ] `src/provider/schema.ts`
- [ ] `src/pty/schema.ts`
- [ ] `src/question/schema.ts`
- [ ] `src/session/schema.ts`
- [ ] `src/sync/schema.ts`
- [ ] `src/tool/schema.ts`
- [x] `src/account/schema.ts`
- [x] `src/control-plane/schema.ts`
- [x] `src/permission/schema.ts`
- [x] `src/project/schema.ts`
- [x] `src/provider/schema.ts`
- [x] `src/pty/schema.ts`
- [x] `src/question/schema.ts`
- [x] `src/session/schema.ts`
- [x] `src/storage/schema.ts`
- [x] `src/sync/schema.ts`
- [x] `src/tool/schema.ts`
- [x] `src/util/schema.ts`
### Session domain
Major cluster. Message + event types flow through the SSE API and every SDK
output, so byte-identical SDK surface is critical.
- [ ] `src/session/compaction.ts`
- [ ] `src/session/message-v2.ts`
- [ ] `src/session/message.ts`
- [ ] `src/session/prompt.ts`
- [ ] `src/session/revert.ts`
- [ ] `src/session/session.ts`
- [ ] `src/session/status.ts`
- [ ] `src/session/summary.ts`
- [ ] `src/session/todo.ts`
Suggested order for this cluster, starting from the leaves that `session.ts`
and the SSE/event surface depend on:
1. `src/session/schema.ts` ✅ already migrated
2. `src/provider/schema.ts` if `message-v2.ts` still relies on zod-first IDs
3. `src/lsp/*` schema leaves needed by `LSP.Range`
4. `src/snapshot/*` leaves used by `Snapshot.FileDiff`
5. `src/session/message-v2.ts`
6. `src/session/message.ts`
7. `src/session/prompt.ts`
8. `src/session/revert.ts`
9. `src/session/summary.ts`
10. `src/session/status.ts`
11. `src/session/todo.ts`
12. `src/session/session.ts`
13. `src/session/compaction.ts`
Dependency sketch:
```text
session.ts
|- project/schema.ts
|- control-plane/schema.ts
|- permission/schema.ts
|- snapshot/*
|- message-v2.ts
| |- provider/schema.ts
| |- lsp/*
| |- snapshot/*
| |- sync/index.ts
| `- bus/bus-event.ts
|- sync/index.ts
|- bus/bus-event.ts
`- util/update-schema.ts
```
Working rule for this cluster:
- migrate reusable leaf schemas and nested payload objects first
- migrate aggregate DTOs like `Session.Info` after their nested pieces exist as
named Schema values
- leave zod-only event/update helpers in place temporarily when converting
them would force unrelated churn across sync/bus boundaries
`message-v2.ts` first-pass outline:
1. Schema-backed imports already available
- `SessionID`, `MessageID`, `PartID`
- `ProviderID`, `ModelID`
2. Local leaf objects to extract and migrate first
- output format payloads
- common part bases like `PartBase`
- timestamp/range helper objects like `time.start/end`
- file/source helper objects
- token/cost/model helper objects
3. Part variants built from those leaves
- `SnapshotPart`, `PatchPart`, `TextPart`, `ReasoningPart`
- `FilePart`, `AgentPart`, `CompactionPart`, `SubtaskPart`
- retry/step/tool related parts
4. Higher-level unions and DTOs
- `FilePartSource`
- part unions
- message unions and assistant/user payloads
5. Errors and event payloads last
- `NamedError.create(...)` shapes can stay temporarily if converting them to
`Schema.TaggedErrorClass` would force unrelated churn
- `SyncEvent.define(...)` and `BusEvent.define(...)` payloads can keep using
derived `.zod` until the sync/bus layers are migrated
Possible later tightening after the Schema-first migration is stable:
- promote repeated opaque strings and timestamp numbers into branded/newtype
leaf schemas where that adds domain value without changing the wire format
- [x] `src/session/compaction.ts`
- [x] `src/session/message-v2.ts`
- [x] `src/session/message.ts`
- [x] `src/session/prompt.ts`
- [x] `src/session/revert.ts`
- [x] `src/session/session.ts`
- [x] `src/session/status.ts`
- [x] `src/session/summary.ts`
- [x] `src/session/todo.ts`
### Provider domain
+2 -2
View File
@@ -372,7 +372,7 @@ export class Agent implements ACPAgent {
}
if (part.tool === "todowrite") {
const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
const parsedTodos = z.array(Todo.Info.zod).safeParse(JSON.parse(part.state.output))
if (parsedTodos.success) {
await this.connection
.sessionUpdate({
@@ -901,7 +901,7 @@ export class Agent implements ACPAgent {
}
if (part.tool === "todowrite") {
const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
const parsedTodos = z.array(Todo.Info.zod).safeParse(JSON.parse(part.state.output))
if (parsedTodos.success) {
await this.connection
.sessionUpdate({
@@ -1,16 +1,9 @@
You are a helpful AI assistant tasked with summarizing conversations.
You are an anchored context summarization assistant for coding sessions.
When asked to summarize, provide a detailed but concise summary of the older conversation history.
The most recent turns may be preserved verbatim outside your summary, so focus on information that would still be needed to continue the work with that recent context available.
Focus on information that would be helpful for continuing the conversation, including:
- What was done
- What is currently being worked on
- Which files are being modified
- What needs to be done next
- Key user requests, constraints, or preferences that should persist
- Important technical decisions and why they were made
Summarize only the conversation history you are given. The newest turns may be kept verbatim outside your summary, so focus on the older context that still matters for continuing the work.
Your summary should be comprehensive enough to provide context but concise enough to be quickly understood.
If the prompt includes a <previous-summary> block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts.
Do not respond to any questions in the conversation, only output the summary.
Respond in the same language the user used in the conversation.
Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths and identifiers when known, and prefer terse bullets over paragraphs.
Do not answer the conversation itself. Do not mention that you are summarizing, compacting, or merging context. Respond in the same language as the conversation.
+1 -2
View File
@@ -23,8 +23,7 @@ const DiagnosticsCommand = cmd({
const out = await AppRuntime.runPromise(
LSP.Service.use((lsp) =>
Effect.gen(function* () {
yield* lsp.touchFile(args.file, true)
yield* Effect.sleep(1000)
yield* lsp.touchFile(args.file, "full")
return yield* lsp.diagnostics()
}),
),
+4 -2
View File
@@ -985,7 +985,8 @@ export const GithubRunCommand = cmd({
const err = result.info.error
console.error("Agent error:", err)
if (err.name === "ContextOverflowError") throw new Error(formatPromptTooLargeError(files))
throw new Error(`${err.name}: ${err.data?.message || ""}`)
const message = "message" in err.data ? err.data.message : ""
throw new Error(`${err.name}: ${message}`)
}
const text = extractResponseText(result.parts)
@@ -1014,7 +1015,8 @@ export const GithubRunCommand = cmd({
const err = summary.info.error
console.error("Summary agent error:", err)
if (err.name === "ContextOverflowError") throw new Error(formatPromptTooLargeError(files))
throw new Error(`${err.name}: ${err.data?.message || ""}`)
const message = "message" in err.data ? err.data.message : ""
throw new Error(`${err.name}: ${message}`)
}
const summaryText = extractResponseText(summary.parts)
+5 -4
View File
@@ -11,6 +11,7 @@ import { ShareNext } from "../../share"
import { EOL } from "os"
import { Filesystem } from "../../util"
import { AppRuntime } from "@/effect/app-runtime"
import { Schema } from "effect"
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
export type ShareData =
@@ -154,10 +155,10 @@ export const ImportCommand = cmd({
return
}
const info = Session.Info.parse({
const info = Schema.decodeUnknownSync(Session.Info)({
...exportData.info,
projectID: Instance.project.id,
})
}) as Session.Info
const row = Session.toRow(info)
Database.use((db) =>
db
@@ -168,7 +169,7 @@ export const ImportCommand = cmd({
)
for (const msg of exportData.messages) {
const msgInfo = MessageV2.Info.parse(msg.info)
const msgInfo = MessageV2.Info.zod.parse(msg.info)
const { id, sessionID: _, ...msgData } = msgInfo
Database.use((db) =>
db
@@ -184,7 +185,7 @@ export const ImportCommand = cmd({
)
for (const part of msg.parts) {
const partInfo = MessageV2.Part.parse(part)
const partInfo = MessageV2.Part.zod.parse(part)
const { id: partId, sessionID: _s, messageID, ...partData } = partInfo
Database.use((db) =>
db
@@ -3,6 +3,8 @@ import { UI } from "@/cli/ui"
import { tui } from "./app"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
import { errorMessage } from "@/util/error"
import { validateSession } from "./validate-session"
export const AttachCommand = cmd({
command: "attach <url>",
@@ -65,6 +67,20 @@ export const AttachCommand = cmd({
return { Authorization: auth }
})()
const config = await TuiConfig.get()
try {
await validateSession({
url: args.url,
sessionID: args.session,
directory,
headers,
})
} catch (error) {
UI.error(errorMessage(error))
process.exitCode = 1
return
}
await tui({
url: args.url,
config,
@@ -68,6 +68,7 @@ import { Flag } from "@/flag/flag"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
import parsers from "../../../../../../parsers-config.ts"
import * as Clipboard from "../../util/clipboard"
import { errorMessage } from "@/util/error"
import { Toast, useToast } from "../../ui/toast"
import { useKV } from "../../context/kv.tsx"
import * as Editor from "../../util/editor"
@@ -180,31 +181,43 @@ export function Session() {
const toast = useToast()
const sdk = useSDK()
createEffect(async () => {
const previousWorkspace = project.workspace.current()
const result = await sdk.client.session.get({ sessionID: route.sessionID }, { throwOnError: true })
if (!result.data) {
createEffect(() => {
const sessionID = route.sessionID
void (async () => {
const previousWorkspace = project.workspace.current()
const result = await sdk.client.session.get({ sessionID }, { throwOnError: true })
if (!result.data) {
toast.show({
message: `Session not found: ${sessionID}`,
variant: "error",
duration: 5000,
})
navigate({ type: "home" })
return
}
if (result.data.workspaceID !== previousWorkspace) {
project.workspace.set(result.data.workspaceID)
// Sync all the data for this workspace. Note that this
// workspace may not exist anymore which is why this is not
// fatal. If it doesn't we still want to show the session
// (which will be non-interactive)
try {
await sync.bootstrap({ fatal: false })
} catch {}
}
await sync.session.sync(sessionID)
if (route.sessionID === sessionID && scroll) scroll.scrollBy(100_000)
})().catch((error) => {
if (route.sessionID !== sessionID) return
toast.show({
message: `Session not found: ${route.sessionID}`,
message: errorMessage(error),
variant: "error",
duration: 5000,
})
navigate({ type: "home" })
return
}
if (result.data.workspaceID !== previousWorkspace) {
project.workspace.set(result.data.workspaceID)
// Sync all the data for this workspace. Note that this
// workspace may not exist anymore which is why this is not
// fatal. If it doesn't we still want to show the session
// (which will be non-interactive)
try {
await sync.bootstrap({ fatal: false })
} catch (e) {}
}
await sync.session.sync(route.sessionID)
if (scroll) scroll.scrollBy(100_000)
})
})
let lastSwitch: string | undefined = undefined
@@ -1237,7 +1250,17 @@ function UserMessage(props: {
}) {
const ctx = use()
const local = useLocal()
const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0])
const text = createMemo(() => {
const texts = props.parts
.map((x) => {
if (x.type === "text" && !x.synthetic) {
return x.text
}
return null
})
.filter(Boolean)
return texts.join("\n\n")
})
const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : [])))
const { theme } = useTheme()
const [hover, setHover] = createSignal(false)
@@ -1272,7 +1295,7 @@ function UserMessage(props: {
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
flexShrink={0}
>
<text fg={theme.text}>{text()?.text}</text>
<text fg={theme.text}>{text()}</text>
<Show when={files().length}>
<box flexDirection="row" paddingBottom={metadataVisible() ? 1 : 0} paddingTop={1} gap={1} flexWrap="wrap">
<For each={files()}>
@@ -16,6 +16,7 @@ import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
import { writeHeapSnapshot } from "v8"
import { TuiConfig } from "./config/tui"
import { OPENCODE_PROCESS_ROLE, OPENCODE_RUN_ID, ensureRunID, sanitizedProcessEnv } from "@/util/opencode-process"
import { validateSession } from "./validate-session"
declare global {
const OPENCODE_WORKER_PATH: string
@@ -202,6 +203,19 @@ export const TuiThreadCommand = cmd({
events: createEventSource(client),
}
try {
await validateSession({
url: transport.url,
sessionID: args.session,
directory: cwd,
fetch: transport.fetch,
})
} catch (error) {
UI.error(errorMessage(error))
process.exitCode = 1
return
}
setTimeout(() => {
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
}, 1000).unref?.()
@@ -0,0 +1,24 @@
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import { SessionID } from "@/session/schema"
export async function validateSession(input: {
url: string
sessionID?: string
directory?: string
fetch?: typeof fetch
headers?: RequestInit["headers"]
}) {
if (!input.sessionID) return
const result = SessionID.zod.safeParse(input.sessionID)
if (!result.success) {
throw new Error(`Invalid session ID: ${result.error.issues.at(0)?.message ?? "unknown error"}`)
}
await createOpencodeClient({
baseUrl: input.url,
directory: input.directory,
fetch: input.fetch,
headers: input.headers,
}).session.get({ sessionID: result.data }, { throwOnError: true })
}
+1 -1
View File
@@ -7,6 +7,7 @@ import { InstallationVersion } from "@/installation/version"
export async function upgrade() {
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal()))
if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return
const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
const latest = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest(method))).catch(() => {})
if (!latest) return
@@ -17,7 +18,6 @@ export async function upgrade() {
}
if (InstallationVersion === latest) return
if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return
const kind = Installation.getReleaseType(InstallationVersion, latest)
@@ -1,8 +1,7 @@
import { Schema } from "effect"
import z from "zod"
import { Identifier } from "@/id/id"
import { ZodOverride } from "@/util/effect-zod"
import { zod, ZodOverride } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const workspaceIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("workspace") }).pipe(
@@ -14,6 +13,6 @@ export type WorkspaceID = typeof workspaceIdSchema.Type
export const WorkspaceID = workspaceIdSchema.pipe(
withStatics((schema: typeof workspaceIdSchema) => ({
ascending: (id?: string) => schema.make(Identifier.ascending("workspace", id)),
zod: Identifier.schema("workspace").pipe(z.custom<WorkspaceID>()),
zod: zod(schema),
})),
)
+11 -6
View File
@@ -25,7 +25,7 @@ export type Status = z.infer<typeof Status>
export interface Interface {
readonly init: () => Effect.Effect<void>
readonly status: () => Effect.Effect<Status[]>
readonly file: (filepath: string) => Effect.Effect<void>
readonly file: (filepath: string) => Effect.Effect<boolean>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Format") {}
@@ -70,16 +70,19 @@ export const layer = Layer.effect(
}
}),
)
return checks.filter((x) => x.cmd).map((x) => ({ item: x.item, cmd: x.cmd! }))
return checks
.filter((x): x is { item: Formatter.Info; cmd: string[] } => x.cmd !== false)
.map((x) => ({ item: x.item, cmd: x.cmd }))
}
function formatFile(filepath: string) {
return Effect.gen(function* () {
log.info("formatting", { file: filepath })
const ext = path.extname(filepath)
const formatters = yield* Effect.promise(() => getFormatter(path.extname(filepath)))
for (const { item, cmd } of yield* Effect.promise(() => getFormatter(ext))) {
if (cmd === false) continue
if (!formatters.length) return false
for (const { item, cmd } of formatters) {
log.info("running", { command: cmd })
const replaced = cmd.map((x) => x.replace("$FILE", filepath))
const dir = yield* InstanceState.directory
@@ -113,6 +116,8 @@ export const layer = Layer.effect(
})
}
}
return true
})
}
@@ -188,7 +193,7 @@ export const layer = Layer.effect(
const file = Effect.fn("Format.file")(function* (filepath: string) {
const { formatFile } = yield* InstanceState.get(state)
yield* formatFile(filepath)
return yield* formatFile(filepath)
})
return Service.of({ init, status, file })
+12 -9
View File
@@ -132,6 +132,17 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildPro
Effect.catch(() => Effect.succeed({ code: ChildProcessSpawner.ExitCode(1), stdout: "", stderr: "" })),
)
const viewVersion = Effect.fnUntraced(function* (method: "npm" | "pnpm" | "bun", spec: string) {
const args = method === "bun" ? ["pm", "view", spec, "version", "--json"] : ["view", spec, "version", "--json"]
const result = yield* run([method, ...args])
if (result.code !== 0 || !result.stdout.trim()) {
return yield* new UpgradeFailedError({
stderr: result.stderr || result.stdout || `Failed to resolve ${spec}`,
})
}
return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String))(result.stdout)
})
const getBrewFormula = Effect.fnUntraced(function* () {
const tapFormula = yield* text(["brew", "list", "--formula", "anomalyco/tap/opencode"])
if (tapFormula.includes("opencode")) return "anomalyco/tap/opencode"
@@ -217,15 +228,7 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildPro
}
if (detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm") {
const r = (yield* text(["npm", "config", "get", "registry"])).trim()
const reg = r || "https://registry.npmjs.org"
const registry = reg.endsWith("/") ? reg.slice(0, -1) : reg
const channel = InstallationChannel
const response = yield* httpOk.execute(
HttpClientRequest.get(`${registry}/opencode-ai/${channel}`).pipe(HttpClientRequest.acceptJson),
)
const data = yield* HttpClientResponse.schemaBodyJson(NpmPackage)(response)
return data.version
return yield* viewVersion(detectedMethod, `opencode-ai@${InstallationChannel}`)
}
if (detectedMethod === "choco") {
+514 -69
View File
@@ -14,6 +14,16 @@ import { withTimeout } from "../util/timeout"
import { Filesystem } from "../util"
const DIAGNOSTICS_DEBOUNCE_MS = 150
const DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS = 5_000
const DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS = 10_000
const DIAGNOSTICS_REQUEST_TIMEOUT_MS = 3_000
const INITIALIZE_TIMEOUT_MS = 45_000
// LSP spec constants
const FILE_CHANGE_CREATED = 1
const FILE_CHANGE_CHANGED = 2
const TEXT_DOCUMENT_SYNC_INCREMENTAL = 2
const log = Log.create({ service: "lsp.client" })
@@ -38,48 +48,194 @@ export const Event = {
),
}
type DocumentDiagnosticReport = {
items?: Diagnostic[]
relatedDocuments?: Record<string, DocumentDiagnosticReport>
}
type WorkspaceDiagnosticReport = {
items?: {
uri?: string
items?: Diagnostic[]
}[]
}
type DiagnosticRequestResult = {
handled: boolean
matched: boolean
byFile: Map<string, Diagnostic[]>
}
type CapabilityRegistration = {
id: string
method: string
registerOptions?: {
identifier?: string
workspaceDiagnostics?: boolean
}
}
type ServerCapabilities = {
textDocumentSync?:
| number
| {
change?: number
}
diagnosticProvider?: unknown
[key: string]: unknown
}
function getFilePath(uri: string) {
if (!uri.startsWith("file://")) return
return Filesystem.normalizePath(fileURLToPath(uri))
}
function getSyncKind(capabilities?: ServerCapabilities) {
if (!capabilities) return
const sync = capabilities.textDocumentSync
if (typeof sync === "number") return sync
return sync?.change
}
function endPosition(text: string) {
const lines = text.split(/\r\n|\r|\n/)
return {
line: lines.length - 1,
character: lines.at(-1)?.length ?? 0,
}
}
function dedupeDiagnostics(items: Diagnostic[]) {
const seen = new Set<string>()
return items.filter((item) => {
const key = JSON.stringify({
code: item.code,
severity: item.severity,
message: item.message,
source: item.source,
range: item.range,
})
if (seen.has(key)) return false
seen.add(key)
return true
})
}
function configurationValue(settings: unknown, section?: string) {
if (!section) return settings ?? null
const result = section.split(".").reduce<unknown>((acc, key) => {
if (!acc || typeof acc !== "object" || !(key in acc)) return undefined
return (acc as Record<string, unknown>)[key]
}, settings)
return result ?? null
}
// TypeScript's built-in LSP pushes diagnostics aggressively on first open.
// We seed the push cache on the very first publish so waitForFreshPush can
// resolve immediately instead of waiting for a second debounced push.
function shouldSeedDiagnosticsOnFirstPush(serverID: string) {
return serverID === "typescript"
}
export async function create(input: { serverID: string; server: LSPServer.Handle; root: string; directory: string }) {
const l = log.clone().tag("serverID", input.serverID)
l.info("starting client")
const logger = log.clone().tag("serverID", input.serverID)
logger.info("starting client")
const connection = createMessageConnection(
new StreamMessageReader(input.server.process.stdout as any),
new StreamMessageWriter(input.server.process.stdin as any),
)
// Server stderr can contain both real errors and routine informational logs,
// which is normal stderr practice for some tools. Keep the raw stream at
// debug so users can opt in with --print-logs --log-level DEBUG without
// polluting normal logs.
input.server.process.stderr?.on("data", (data: Buffer) => {
const text = data.toString().trim()
if (text) logger.debug("server stderr", { text: text.slice(0, 1000) })
})
// --- Connection state ---
const pushDiagnostics = new Map<string, Diagnostic[]>()
const pullDiagnostics = new Map<string, Diagnostic[]>()
const published = new Map<string, { at: number; version?: number }>()
const diagnosticRegistrations = new Map<string, CapabilityRegistration>()
const registrationListeners = new Set<() => void>()
const mergedDiagnostics = (filePath: string) =>
dedupeDiagnostics([...(pushDiagnostics.get(filePath) ?? []), ...(pullDiagnostics.get(filePath) ?? [])])
const updatePushDiagnostics = (filePath: string, next: Diagnostic[]) => {
pushDiagnostics.set(filePath, next)
Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID })
}
const updatePullDiagnostics = (filePath: string, next: Diagnostic[]) => {
pullDiagnostics.set(filePath, next)
}
const emitRegistrationChange = () => {
for (const listener of [...registrationListeners]) listener()
}
// --- LSP connection handlers ---
const diagnostics = new Map<string, Diagnostic[]>()
connection.onNotification("textDocument/publishDiagnostics", (params) => {
const filePath = Filesystem.normalizePath(fileURLToPath(params.uri))
l.info("textDocument/publishDiagnostics", {
const filePath = getFilePath(params.uri)
if (!filePath) return
logger.info("textDocument/publishDiagnostics", {
path: filePath,
count: params.diagnostics.length,
version: params.version,
})
const exists = diagnostics.has(filePath)
diagnostics.set(filePath, params.diagnostics)
if (!exists && input.serverID === "typescript") return
Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID })
published.set(filePath, {
at: Date.now(),
version: typeof params.version === "number" ? params.version : undefined,
})
if (shouldSeedDiagnosticsOnFirstPush(input.serverID) && !pushDiagnostics.has(filePath)) {
pushDiagnostics.set(filePath, params.diagnostics)
return
}
updatePushDiagnostics(filePath, params.diagnostics)
})
connection.onRequest("window/workDoneProgress/create", (params) => {
l.info("window/workDoneProgress/create", params)
logger.info("window/workDoneProgress/create", params)
return null
})
connection.onRequest("workspace/configuration", async () => {
// Return server initialization options
return [input.server.initialization ?? {}]
connection.onRequest("workspace/configuration", async (params) => {
const items = (params as { items?: { section?: string }[] }).items ?? []
return items.map((item) => configurationValue(input.server.initialization, item.section))
})
connection.onRequest("client/registerCapability", async (params) => {
const registrations = (params as { registrations?: CapabilityRegistration[] }).registrations ?? []
let changed = false
for (const registration of registrations) {
if (registration.method !== "textDocument/diagnostic") continue
diagnosticRegistrations.set(registration.id, registration)
changed = true
}
if (changed) emitRegistrationChange()
})
connection.onRequest("client/unregisterCapability", async (params) => {
const registrations = (params as { unregisterations?: { id: string; method: string }[] }).unregisterations ?? []
let changed = false
for (const registration of registrations) {
if (registration.method !== "textDocument/diagnostic") continue
diagnosticRegistrations.delete(registration.id)
changed = true
}
if (changed) emitRegistrationChange()
})
connection.onRequest("client/registerCapability", async () => {})
connection.onRequest("client/unregisterCapability", async () => {})
connection.onRequest("workspace/workspaceFolders", async () => [
{
name: "workspace",
uri: pathToFileURL(input.root).href,
},
])
connection.onRequest("workspace/diagnostic/refresh", async () => null)
connection.listen()
l.info("sending initialize")
await withTimeout(
connection.sendRequest("initialize", {
// --- Initialize handshake ---
logger.info("sending initialize")
const initialized = await withTimeout(
connection.sendRequest<{ capabilities?: ServerCapabilities }>("initialize", {
rootUri: pathToFileURL(input.root).href,
processId: input.server.process.pid,
workspaceFolders: [
@@ -100,21 +256,28 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
didChangeWatchedFiles: {
dynamicRegistration: true,
},
diagnostics: {
refreshSupport: false,
},
},
textDocument: {
synchronization: {
didOpen: true,
didChange: true,
},
diagnostic: {
dynamicRegistration: true,
relatedDocumentSupport: true,
},
publishDiagnostics: {
versionSupport: true,
versionSupport: false,
},
},
},
}),
45_000,
INITIALIZE_TIMEOUT_MS,
).catch((err) => {
l.error("initialize error", { error: err })
logger.error("initialize error", { error: err })
throw new InitializeError(
{ serverID: input.serverID },
{
@@ -123,6 +286,9 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
)
})
const syncKind = getSyncKind(initialized.capabilities)
const hasStaticPullDiagnostics = Boolean(initialized.capabilities?.diagnosticProvider)
await connection.sendNotification("initialized", {})
if (input.server.initialization) {
@@ -131,9 +297,280 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
})
}
const files: {
[path: string]: number
} = {}
const files: Record<string, { version: number; text: string }> = {}
// --- Diagnostic helpers ---
const mergeResults = (filePath: string, results: DiagnosticRequestResult[]) => {
const handled = results.some((result) => result.handled)
const matched = results.some((result) => result.matched)
if (!handled) return { handled: false, matched: false }
const merged = new Map<string, Diagnostic[]>()
for (const result of results) {
for (const [target, items] of result.byFile.entries()) {
const existing = merged.get(target) ?? []
merged.set(target, existing.concat(items))
}
}
if (matched && !merged.has(filePath)) merged.set(filePath, [])
for (const [target, items] of merged.entries()) {
updatePullDiagnostics(target, dedupeDiagnostics(items))
}
return { handled, matched }
}
async function requestDiagnosticReport(filePath: string, identifier?: string): Promise<DiagnosticRequestResult> {
const report = await withTimeout(
connection.sendRequest<DocumentDiagnosticReport | null>("textDocument/diagnostic", {
...(identifier ? { identifier } : {}),
textDocument: {
uri: pathToFileURL(filePath).href,
},
}),
DIAGNOSTICS_REQUEST_TIMEOUT_MS,
).catch(() => null)
if (!report) return { handled: false, matched: false, byFile: new Map<string, Diagnostic[]>() }
const byFile = new Map<string, Diagnostic[]>()
const push = (target: string, items: Diagnostic[]) => {
const existing = byFile.get(target) ?? []
byFile.set(target, existing.concat(items))
}
let handled = false
let matched = false
if (Array.isArray(report.items)) {
push(filePath, report.items)
handled = true
matched = true
}
for (const [uri, related] of Object.entries(report.relatedDocuments ?? {})) {
const relatedPath = getFilePath(uri)
if (!relatedPath || !Array.isArray(related.items)) continue
push(relatedPath, related.items)
handled = true
matched = matched || relatedPath === filePath
}
return { handled, matched, byFile }
}
async function requestWorkspaceDiagnosticReport(
filePath: string,
identifier?: string,
): Promise<DiagnosticRequestResult> {
const report = await withTimeout(
connection.sendRequest<WorkspaceDiagnosticReport | null>("workspace/diagnostic", {
...(identifier ? { identifier } : {}),
previousResultIds: [],
}),
DIAGNOSTICS_REQUEST_TIMEOUT_MS,
).catch(() => null)
if (!report) return { handled: false, matched: false, byFile: new Map<string, Diagnostic[]>() }
const byFile = new Map<string, Diagnostic[]>()
let matched = false
for (const item of report.items ?? []) {
const relatedPath = item.uri ? getFilePath(item.uri) : undefined
if (!relatedPath || !Array.isArray(item.items)) continue
const existing = byFile.get(relatedPath) ?? []
byFile.set(relatedPath, existing.concat(item.items))
matched = matched || relatedPath === filePath
}
return { handled: true, matched, byFile }
}
function documentPullState() {
const documentRegistrations = [...diagnosticRegistrations.values()].filter(
(registration) => registration.registerOptions?.workspaceDiagnostics !== true,
)
return {
documentIdentifiers: [
...new Set(documentRegistrations.flatMap((registration) => registration.registerOptions?.identifier ?? [])),
],
supported: hasStaticPullDiagnostics || documentRegistrations.length > 0,
}
}
function workspacePullState() {
const workspaceRegistrations = [...diagnosticRegistrations.values()].filter(
(registration) => registration.registerOptions?.workspaceDiagnostics === true,
)
return {
workspaceIdentifiers: [
...new Set(workspaceRegistrations.flatMap((registration) => registration.registerOptions?.identifier ?? [])),
],
supported: workspaceRegistrations.length > 0,
}
}
const hasCurrentFileDiagnostics = (filePath: string, results: DiagnosticRequestResult[]) =>
results.some((result) => (result.byFile.get(filePath)?.length ?? 0) > 0)
async function requestDiagnostics(
filePath: string,
requests: Promise<DiagnosticRequestResult>[],
done: (results: DiagnosticRequestResult[]) => boolean,
) {
if (!requests.length) return { handled: false, matched: false }
const results: DiagnosticRequestResult[] = []
return new Promise<{ handled: boolean; matched: boolean }>((resolve) => {
let pending = requests.length
let resolved = false
const finish = (merged: { handled: boolean; matched: boolean }, force = false) => {
if (resolved) return
if (!force && !done(results)) return
resolved = true
resolve(merged)
}
for (const request of requests) {
request.then((result) => {
results.push(result)
pending -= 1
const merged = mergeResults(filePath, results)
finish(merged)
if (pending === 0) finish(merged, true)
})
}
})
}
// LATENCY-CRITICAL: dispatch identifier pulls in parallel and unblock once one
// batch already produced diagnostics for the current file. Let slower pulls keep
// merging in the background; do not sequence identifier-by-identifier, and do
// not add a post-match settle/debounce delay. See PR #23771.
async function requestDocumentDiagnostics(filePath: string) {
const state = documentPullState()
if (!state.supported) return { handled: false, matched: false }
return requestDiagnostics(
filePath,
[
requestDiagnosticReport(filePath),
...state.documentIdentifiers.map((identifier) => requestDiagnosticReport(filePath, identifier)),
],
(results) => hasCurrentFileDiagnostics(filePath, results),
)
}
async function requestFullDiagnostics(filePath: string) {
const documentState = documentPullState()
const workspaceState = workspacePullState()
if (!documentState.supported && !workspaceState.supported) return { handled: false, matched: false }
return mergeResults(
filePath,
await Promise.all([
...(documentState.supported ? [requestDiagnosticReport(filePath)] : []),
...documentState.documentIdentifiers.map((identifier) => requestDiagnosticReport(filePath, identifier)),
...(workspaceState.supported ? [requestWorkspaceDiagnosticReport(filePath)] : []),
...workspaceState.workspaceIdentifiers.map((identifier) =>
requestWorkspaceDiagnosticReport(filePath, identifier),
),
]),
)
}
function waitForRegistrationChange(timeout: number) {
if (timeout <= 0) return Promise.resolve(false)
return new Promise<boolean>((resolve) => {
let finished = false
let timer: ReturnType<typeof setTimeout> | undefined
const finish = (result: boolean) => {
if (finished) return
finished = true
if (timer) clearTimeout(timer)
registrationListeners.delete(listener)
resolve(result)
}
const listener = () => finish(true)
registrationListeners.add(listener)
timer = setTimeout(() => finish(false), timeout)
})
}
function waitForFreshPush(request: { path: string; version: number; after: number; timeout: number }) {
if (request.timeout <= 0) return Promise.resolve(false)
return new Promise<boolean>((resolve) => {
let finished = false
let debounceTimer: ReturnType<typeof setTimeout> | undefined
let timeoutTimer: ReturnType<typeof setTimeout> | undefined
let unsub: (() => void) | undefined
const finish = (result: boolean) => {
if (finished) return
finished = true
if (debounceTimer) clearTimeout(debounceTimer)
if (timeoutTimer) clearTimeout(timeoutTimer)
unsub?.()
resolve(result)
}
const schedule = () => {
const hit = published.get(request.path)
if (!hit) return
if (typeof hit.version === "number" && hit.version !== request.version) return
if (hit.at < request.after && hit.version !== request.version) return
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => finish(true), Math.max(0, DIAGNOSTICS_DEBOUNCE_MS - (Date.now() - hit.at)))
}
timeoutTimer = setTimeout(() => finish(false), request.timeout)
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
if (event.properties.path !== request.path || event.properties.serverID !== input.serverID) return
schedule()
})
schedule()
})
}
async function waitForDocumentDiagnostics(request: { path: string; version: number; after?: number }) {
const startedAt = request.after ?? Date.now()
const pushWait = waitForFreshPush({
path: request.path,
version: request.version,
after: startedAt,
timeout: DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS,
})
while (Date.now() - startedAt < DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS) {
const result = await requestDocumentDiagnostics(request.path)
if (result.matched) return
const remaining = DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS - (Date.now() - startedAt)
if (remaining <= 0) return
const next = await Promise.race([
pushWait.then((ready) => (ready ? "push" : ("timeout" as const))),
waitForRegistrationChange(remaining).then((changed) => (changed ? "registration" : ("timeout" as const))),
])
if (next !== "registration") return
}
}
async function waitForFullDiagnostics(request: { path: string; version: number; after?: number }) {
const startedAt = request.after ?? Date.now()
const pushWait = waitForFreshPush({
path: request.path,
version: request.version,
after: startedAt,
timeout: DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS,
})
while (Date.now() - startedAt < DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS) {
const result = await requestFullDiagnostics(request.path)
if (result.handled || result.matched) return
const remaining = DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS - (Date.now() - startedAt)
if (remaining <= 0) return
const next = await Promise.race([
pushWait.then((ready) => (ready ? "push" : ("timeout" as const))),
waitForRegistrationChange(remaining).then((changed) => (changed ? "registration" : ("timeout" as const))),
])
if (next !== "registration") return
}
}
// --- Public API ---
const result = {
root: input.root,
@@ -145,26 +582,32 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
},
notify: {
async open(request: { path: string }) {
request.path = path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path)
request.path = Filesystem.normalizePath(
path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path),
)
const text = await Filesystem.readText(request.path)
const extension = path.extname(request.path)
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
const version = files[request.path]
if (version !== undefined) {
log.info("workspace/didChangeWatchedFiles", request)
const document = files[request.path]
if (document !== undefined) {
// Do not wipe diagnostics on didChange. Some servers (e.g. clangd) only
// re-emit diagnostics when the content actually changes, so clearing
// here would lose errors for no-op touchFile calls. Let the server's
// next push/pull overwrite naturally.
logger.info("workspace/didChangeWatchedFiles", request)
await connection.sendNotification("workspace/didChangeWatchedFiles", {
changes: [
{
uri: pathToFileURL(request.path).href,
type: 2, // Changed
type: FILE_CHANGE_CHANGED,
},
],
})
const next = version + 1
files[request.path] = next
log.info("textDocument/didChange", {
const next = document.version + 1
files[request.path] = { version: next, text }
logger.info("textDocument/didChange", {
path: request.path,
version: next,
})
@@ -173,23 +616,35 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
uri: pathToFileURL(request.path).href,
version: next,
},
contentChanges: [{ text }],
contentChanges:
syncKind === TEXT_DOCUMENT_SYNC_INCREMENTAL
? [
{
range: {
start: { line: 0, character: 0 },
end: endPosition(document.text),
},
text,
},
]
: [{ text }],
})
return
return next
}
log.info("workspace/didChangeWatchedFiles", request)
logger.info("workspace/didChangeWatchedFiles", request)
await connection.sendNotification("workspace/didChangeWatchedFiles", {
changes: [
{
uri: pathToFileURL(request.path).href,
type: 1, // Created
type: FILE_CHANGE_CREATED,
},
],
})
log.info("textDocument/didOpen", request)
diagnostics.delete(request.path)
logger.info("textDocument/didOpen", request)
pushDiagnostics.delete(request.path)
pullDiagnostics.delete(request.path)
await connection.sendNotification("textDocument/didOpen", {
textDocument: {
uri: pathToFileURL(request.path).href,
@@ -198,52 +653,42 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
text,
},
})
files[request.path] = 0
return
files[request.path] = { version: 0, text }
return 0
},
},
get diagnostics() {
return diagnostics
const result = new Map<string, Diagnostic[]>()
for (const key of new Set([...pushDiagnostics.keys(), ...pullDiagnostics.keys()])) {
result.set(key, mergedDiagnostics(key))
}
return result
},
async waitForDiagnostics(request: { path: string }) {
async waitForDiagnostics(request: { path: string; version: number; mode?: "document" | "full"; after?: number }) {
const normalizedPath = Filesystem.normalizePath(
path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path),
)
log.info("waiting for diagnostics", { path: normalizedPath })
let unsub: () => void
let debounceTimer: ReturnType<typeof setTimeout> | undefined
return await withTimeout(
new Promise<void>((resolve) => {
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
if (event.properties.path === normalizedPath && event.properties.serverID === result.serverID) {
// Debounce to allow LSP to send follow-up diagnostics (e.g., semantic after syntax)
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
log.info("got diagnostics", { path: normalizedPath })
unsub?.()
resolve()
}, DIAGNOSTICS_DEBOUNCE_MS)
}
})
}),
3000,
)
.catch(() => {})
.finally(() => {
if (debounceTimer) clearTimeout(debounceTimer)
unsub?.()
})
logger.info("waiting for diagnostics", {
path: normalizedPath,
mode: request.mode ?? "full",
version: request.version,
})
if (request.mode === "document") {
await waitForDocumentDiagnostics({ path: normalizedPath, version: request.version, after: request.after })
return
}
await waitForFullDiagnostics({ path: normalizedPath, version: request.version, after: request.after })
},
async shutdown() {
l.info("shutting down")
logger.info("shutting down")
connection.end()
connection.dispose()
await Process.stop(input.server.process)
l.info("shutdown")
logger.info("shutdown")
},
}
l.info("initialized")
logger.info("initialized")
return result
}
+57 -56
View File
@@ -10,9 +10,11 @@ import { Config } from "../config"
import { Flag } from "@/flag/flag"
import { Process } from "../util"
import { spawn as lspspawn } from "./launch"
import { Effect, Layer, Context } from "effect"
import { Effect, Layer, Context, Schema } from "effect"
import { InstanceState } from "@/effect"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { withStatics } from "@/util/schema"
import { zod, ZodOverride } from "@/util/effect-zod"
const log = Log.create({ service: "lsp" })
@@ -20,60 +22,53 @@ export const Event = {
Updated: BusEvent.define("lsp.updated", z.object({})),
}
export const Range = z
.object({
start: z.object({
line: z.number(),
character: z.number(),
}),
end: z.object({
line: z.number(),
character: z.number(),
}),
})
.meta({
ref: "Range",
})
export type Range = z.infer<typeof Range>
const Position = Schema.Struct({
line: Schema.Number,
character: Schema.Number,
})
export const Symbol = z
.object({
name: z.string(),
kind: z.number(),
location: z.object({
uri: z.string(),
range: Range,
}),
})
.meta({
ref: "Symbol",
})
export type Symbol = z.infer<typeof Symbol>
export const Range = Schema.Struct({
start: Position,
end: Position,
})
.annotate({ identifier: "Range" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type Range = typeof Range.Type
export const DocumentSymbol = z
.object({
name: z.string(),
detail: z.string().optional(),
kind: z.number(),
export const Symbol = Schema.Struct({
name: Schema.String,
kind: Schema.Number,
location: Schema.Struct({
uri: Schema.String,
range: Range,
selectionRange: Range,
})
.meta({
ref: "DocumentSymbol",
})
export type DocumentSymbol = z.infer<typeof DocumentSymbol>
}),
})
.annotate({ identifier: "Symbol" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type Symbol = typeof Symbol.Type
export const Status = z
.object({
id: z.string(),
name: z.string(),
root: z.string(),
status: z.union([z.literal("connected"), z.literal("error")]),
})
.meta({
ref: "LSPStatus",
})
export type Status = z.infer<typeof Status>
export const DocumentSymbol = Schema.Struct({
name: Schema.String,
detail: Schema.optional(Schema.String),
kind: Schema.Number,
range: Range,
selectionRange: Range,
})
.annotate({ identifier: "DocumentSymbol" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type DocumentSymbol = typeof DocumentSymbol.Type
export const Status = Schema.Struct({
id: Schema.String,
name: Schema.String,
root: Schema.String,
status: Schema.Literals(["connected", "error"]).annotate({
[ZodOverride]: z.union([z.literal("connected"), z.literal("error")]),
}),
})
.annotate({ identifier: "LSPStatus" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type Status = typeof Status.Type
enum SymbolKind {
File = 1,
@@ -141,7 +136,7 @@ export interface Interface {
readonly init: () => Effect.Effect<void>
readonly status: () => Effect.Effect<Status[]>
readonly hasClients: (file: string) => Effect.Effect<boolean>
readonly touchFile: (input: string, waitForDiagnostics?: boolean) => Effect.Effect<void>
readonly touchFile: (input: string, diagnostics?: "document" | "full") => Effect.Effect<void>
readonly diagnostics: () => Effect.Effect<Record<string, LSPClient.Diagnostic[]>>
readonly hover: (input: LocInput) => Effect.Effect<any>
readonly definition: (input: LocInput) => Effect.Effect<any[]>
@@ -363,15 +358,21 @@ export const layer = Layer.effect(
})
})
const touchFile = Effect.fn("LSP.touchFile")(function* (input: string, waitForDiagnostics?: boolean) {
const touchFile = Effect.fn("LSP.touchFile")(function* (input: string, diagnostics?: "document" | "full") {
log.info("touching file", { file: input })
const clients = yield* getClients(input)
yield* Effect.promise(() =>
Promise.all(
clients.map(async (client) => {
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
await client.notify.open({ path: input })
return wait
const after = Date.now()
const version = await client.notify.open({ path: input })
if (!diagnostics) return
return client.waitForDiagnostics({
path: input,
version,
mode: diagnostics,
after,
})
}),
).catch((err) => {
log.error("failed to touch file", { err, file: input })
+9 -9
View File
@@ -490,7 +490,7 @@ export const Pyright: Info = {
const args = []
if (!binary) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
const resolved = await Npm.which("pyright")
const resolved = await Npm.which("pyright", "pyright-langserver")
if (!resolved) return
binary = resolved
}
@@ -705,32 +705,32 @@ export const CSharp: Info = {
root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
extensions: [".cs"],
async spawn(root) {
let bin = which("csharp-ls")
let bin = which("roslyn-language-server")
if (!bin) {
if (!which("dotnet")) {
log.error(".NET SDK is required to install csharp-ls")
log.error(".NET SDK is required to install roslyn-language-server")
return
}
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("installing csharp-ls via dotnet tool")
const proc = Process.spawn(["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin], {
log.info("installing roslyn-language-server via dotnet tool")
const proc = Process.spawn(["dotnet", "tool", "install", "--global", "roslyn-language-server", "--prerelease"], {
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
log.error("Failed to install csharp-ls")
log.error("Failed to install roslyn-language-server")
return
}
bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed csharp-ls`, { bin })
bin = path.join(Global.Path.bin, "roslyn-language-server" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed roslyn-language-server`, { bin })
}
return {
process: spawn(bin, {
process: spawn(bin, ["--stdio", "--autoLoadProjects"], {
cwd: root,
}),
}
+79 -27
View File
@@ -1,14 +1,19 @@
export * as Npm from "."
import path from "path"
import { fileURLToPath } from "url"
import npa from "npm-package-arg"
import semver from "semver"
import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect"
import Config from "@npmcli/config"
import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js"
import { Effect, Schema, Context, Layer, Option, FileSystem, Stream } from "effect"
import { NodeFileSystem } from "@effect/platform-node"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Global } from "@opencode-ai/shared/global"
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import * as CrossSpawnSpawner from "../effect/cross-spawn-spawner"
import { makeRuntime } from "../effect/runtime"
export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
@@ -34,18 +39,45 @@ export interface Interface {
},
) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError>
readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect<boolean>
readonly which: (pkg: string) => Effect.Effect<Option.Option<string>>
readonly which: (pkg: string, bin?: string) => Effect.Effect<Option.Option<string>>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {}
const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
const npmPath = fileURLToPath(new URL("../..", import.meta.url))
export function sanitize(pkg: string) {
if (!illegal) return pkg
return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
}
const loadOptions = (dir: string) =>
Effect.tryPromise({
try: async () => {
const config = new Config({
npmPath,
cwd: dir,
env: { ...process.env },
argv: [process.execPath, process.execPath],
execPath: process.execPath,
platform: process.platform,
definitions,
flatten,
nerfDarts,
shorthands,
warn: false,
})
await config.load()
return config.flat
},
catch: (cause) =>
new InstallFailedError({
cause,
dir,
}),
})
const resolveEntryPoint = (name: string, dir: string): EntryPoint => {
let entrypoint: Option.Option<string>
try {
@@ -76,12 +108,41 @@ export const layer = Layer.effect(
const global = yield* Global.Service
const fs = yield* FileSystem.FileSystem
const flock = yield* EffectFlock.Service
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg))
const runView = Effect.fnUntraced(function* (cmd: string[]) {
const handle = yield* spawner.spawn(
ChildProcess.make(cmd[0], cmd.slice(1), {
extendEnv: true,
}),
)
const [stdout, stderr] = yield* Effect.all(
[Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
{ concurrency: 2 },
)
const code = yield* handle.exitCode
if (code !== 0 || !stdout.trim()) {
return yield* Effect.fail(stderr || stdout || `Failed to run ${cmd.join(" ")}`)
}
return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String))(stdout)
}, Effect.scoped)
const viewLatestVersion = Effect.fnUntraced(function* (pkg: string) {
return yield* runView(["npm", "view", pkg, "dist-tags.latest", "--json"]).pipe(
Effect.catch(() =>
runView(["pnpm", "view", pkg, "dist-tags.latest", "--json"]).pipe(
Effect.catch(() => runView(["bun", "pm", "view", pkg, "dist-tags.latest", "--json"])),
),
),
)
})
const reify = (input: { dir: string; add?: string[] }) =>
Effect.gen(function* () {
yield* flock.acquire(`npm-install:${input.dir}`)
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"))
const add = input.add ?? []
const npmOptions = yield* loadOptions(input.dir)
const arborist = new Arborist({
...npmOptions,
path: input.dir,
binLinks: true,
progress: false,
@@ -91,14 +152,15 @@ export const layer = Layer.effect(
return yield* Effect.tryPromise({
try: () =>
arborist.reify({
add: input?.add || [],
...npmOptions,
add,
save: true,
saveType: "prod",
}),
catch: (cause) =>
new InstallFailedError({
cause,
add: input?.add,
add,
dir: input.dir,
}),
}) as Effect.Effect<ArboristTree, InstallFailedError>
@@ -109,29 +171,15 @@ export const layer = Layer.effect(
)
const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) {
const response = yield* Effect.tryPromise({
try: () => fetch(`https://registry.npmjs.org/${pkg}`),
catch: () => undefined,
}).pipe(Effect.orElseSucceed(() => undefined))
if (!response || !response.ok) {
return false
}
const data = yield* Effect.tryPromise({
try: () => response.json() as Promise<{ "dist-tags"?: { latest?: string } }>,
catch: () => undefined,
}).pipe(Effect.orElseSucceed(() => undefined))
const latestVersion = data?.["dist-tags"]?.latest
if (!latestVersion) {
const latestVersion = yield* viewLatestVersion(pkg).pipe(Effect.option)
if (Option.isNone(latestVersion)) {
return false
}
const range = /[\s^~*xX<>|=]/.test(cachedVersion)
if (range) return !semver.satisfies(latestVersion, cachedVersion)
if (range) return !semver.satisfies(latestVersion.value, cachedVersion)
return semver.lt(cachedVersion, latestVersion)
return semver.lt(cachedVersion, latestVersion.value)
})
const add = Effect.fn("Npm.add")(function* (pkg: string) {
@@ -207,7 +255,7 @@ export const layer = Layer.effect(
return
}, Effect.scoped)
const which = Effect.fn("Npm.which")(function* (pkg: string) {
const which = Effect.fn("Npm.which")(function* (pkg: string, bin?: string) {
const dir = directory(pkg)
const binDir = path.join(dir, "node_modules", ".bin")
@@ -215,6 +263,9 @@ export const layer = Layer.effect(
const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([] as string[])))
if (files.length === 0) return Option.none<string>()
// Caller picked a specific bin (e.g. pyright exposes both `pyright` and
// `pyright-langserver`); trust the hint if the package provides it.
if (bin) return files.includes(bin) ? Option.some(bin) : Option.none<string>()
if (files.length === 1) return Option.some(files[0])
const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", pkg, "package.json")).pipe(Effect.option)
@@ -223,11 +274,11 @@ export const layer = Layer.effect(
const parsed = pkgJson.value as { bin?: string | Record<string, string> }
if (parsed?.bin) {
const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg
const bin = parsed.bin
if (typeof bin === "string") return Option.some(unscoped)
const keys = Object.keys(bin)
const parsedBin = parsed.bin
if (typeof parsedBin === "string") return Option.some(unscoped)
const keys = Object.keys(parsedBin)
if (keys.length === 1) return Option.some(keys[0])
return bin[unscoped] ? Option.some(unscoped) : Option.some(keys[0])
return parsedBin[unscoped] ? Option.some(unscoped) : Option.some(keys[0])
}
}
@@ -267,6 +318,7 @@ export const defaultLayer = layer.pipe(
Layer.provide(AppFileSystem.layer),
Layer.provide(Global.layer),
Layer.provide(NodeFileSystem.layer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
)
const { runPromise } = makeRuntime(Service, defaultLayer)
+11 -7
View File
@@ -3,6 +3,7 @@ import * as path from "path"
import * as fs from "fs/promises"
import { readFileSync } from "fs"
import { Log } from "../util"
import * as Bom from "../util/bom"
const log = Log.create({ service: "patch" })
@@ -305,18 +306,19 @@ export function maybeParseApplyPatch(
interface ApplyPatchFileUpdate {
unified_diff: string
content: string
bom: boolean
}
export function deriveNewContentsFromChunks(filePath: string, chunks: UpdateFileChunk[]): ApplyPatchFileUpdate {
// Read original file content
let originalContent: string
let originalContent: ReturnType<typeof Bom.split>
try {
originalContent = readFileSync(filePath, "utf-8")
originalContent = Bom.split(readFileSync(filePath, "utf-8"))
} catch (error) {
throw new Error(`Failed to read file ${filePath}: ${error}`, { cause: error })
}
let originalLines = originalContent.split("\n")
let originalLines = originalContent.text.split("\n")
// Drop trailing empty element for consistent line counting
if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") {
@@ -331,14 +333,16 @@ export function deriveNewContentsFromChunks(filePath: string, chunks: UpdateFile
newLines.push("")
}
const newContent = newLines.join("\n")
const next = Bom.split(newLines.join("\n"))
const newContent = next.text
// Generate unified diff
const unifiedDiff = generateUnifiedDiff(originalContent, newContent)
const unifiedDiff = generateUnifiedDiff(originalContent.text, newContent)
return {
unified_diff: unifiedDiff,
content: newContent,
bom: originalContent.bom || next.bom,
}
}
@@ -553,13 +557,13 @@ export async function applyHunksToFiles(hunks: Hunk[]): Promise<AffectedPaths> {
await fs.mkdir(moveDir, { recursive: true })
}
await fs.writeFile(hunk.move_path, fileUpdate.content, "utf-8")
await fs.writeFile(hunk.move_path, Bom.join(fileUpdate.content, fileUpdate.bom), "utf-8")
await fs.unlink(hunk.path)
modified.push(hunk.move_path)
log.info(`Moved file: ${hunk.path} -> ${hunk.move_path}`)
} else {
// Regular update
await fs.writeFile(hunk.path, fileUpdate.content, "utf-8")
await fs.writeFile(hunk.path, Bom.join(fileUpdate.content, fileUpdate.bom), "utf-8")
modified.push(hunk.path)
log.info(`Updated file: ${hunk.path}`)
}
+2 -3
View File
@@ -1,8 +1,7 @@
import { Schema } from "effect"
import z from "zod"
import { Identifier } from "@/id/id"
import { ZodOverride } from "@/util/effect-zod"
import { zod, ZodOverride } from "@/util/effect-zod"
import { Newtype } from "@/util/schema"
export class PermissionID extends Newtype<PermissionID>()(
@@ -13,5 +12,5 @@ export class PermissionID extends Newtype<PermissionID>()(
return this.make(Identifier.ascending("permission", id))
}
static readonly zod = Identifier.schema("permission") as unknown as z.ZodType<PermissionID>
static readonly zod = zod(this)
}
+2
View File
@@ -378,6 +378,8 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
for (const [modelId, model] of Object.entries(provider.models)) {
if (modelId.includes("codex")) continue
if (allowedModels.has(model.api.id)) continue
const match = model.api.id.match(/^gpt-(\d+\.\d+)/)
if (match && parseFloat(match[1]) > 5.4) continue
delete provider.models[modelId]
}
@@ -8,6 +8,7 @@ export const ProjectTable = sqliteTable("project", {
vcs: text(),
name: text(),
icon_url: text(),
icon_url_override: text(),
icon_color: text(),
...Timestamps,
time_initialized: integer(),
+16 -7
View File
@@ -60,7 +60,13 @@ type Row = typeof ProjectTable.$inferSelect
export function fromRow(row: Row): Info {
const icon =
row.icon_url || row.icon_color ? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined } : undefined
row.icon_url || row.icon_url_override || row.icon_color
? {
url: row.icon_url ?? undefined,
override: row.icon_url_override ?? undefined,
color: row.icon_color ?? undefined,
}
: undefined
return {
id: row.id,
worktree: row.worktree,
@@ -207,13 +213,13 @@ export const layer: Layer.Layer<
vcs: fakeVcs,
}
}
const worktree = (() => {
const common = resolveGitPath(sandbox, commonDir.text.trim())
return common === sandbox ? sandbox : pathSvc.dirname(common)
})()
const common = resolveGitPath(sandbox, commonDir.text.trim())
const bareCheck = yield* git(["config", "--bool", "core.bare"], { cwd: sandbox })
const isBareRepo = bareCheck.code === 0 && bareCheck.text.trim() === "true"
const worktree = common === sandbox ? sandbox : isBareRepo ? common : pathSvc.dirname(common)
if (id == null) {
id = yield* readCachedProjectId(pathSvc.join(worktree, ".git"))
id = yield* readCachedProjectId(common)
}
if (!id) {
@@ -226,7 +232,7 @@ export const layer: Layer.Layer<
id = roots[0] ? ProjectID.make(roots[0]) : undefined
if (id) {
yield* fs.writeFileString(pathSvc.join(worktree, ".git", "opencode"), id).pipe(Effect.ignore)
yield* fs.writeFileString(pathSvc.join(common, "opencode"), id).pipe(Effect.ignore)
}
}
@@ -289,6 +295,7 @@ export const layer: Layer.Layer<
vcs: result.vcs ?? null,
name: result.name,
icon_url: result.icon?.url,
icon_url_override: result.icon?.override,
icon_color: result.icon?.color,
time_created: result.time.created,
time_updated: result.time.updated,
@@ -303,6 +310,7 @@ export const layer: Layer.Layer<
vcs: result.vcs ?? null,
name: result.name,
icon_url: result.icon?.url,
icon_url_override: result.icon?.override,
icon_color: result.icon?.color,
time_updated: result.time.updated,
time_initialized: result.time.initialized,
@@ -365,6 +373,7 @@ export const layer: Layer.Layer<
.set({
name: input.name,
icon_url: input.icon?.url,
icon_url_override: input.icon?.override,
icon_color: input.icon?.color,
commands: input.commands,
time_updated: Date.now(),
+2 -2
View File
@@ -1,6 +1,6 @@
import { Schema } from "effect"
import z from "zod"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const projectIdSchema = Schema.String.pipe(Schema.brand("ProjectID"))
@@ -10,6 +10,6 @@ export type ProjectID = typeof projectIdSchema.Type
export const ProjectID = projectIdSchema.pipe(
withStatics((schema: typeof projectIdSchema) => ({
global: schema.make("global"),
zod: z.string().pipe(z.custom<ProjectID>()),
zod: zod(schema),
})),
)
+4 -4
View File
@@ -1,6 +1,6 @@
import { Schema } from "effect"
import z from "zod"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const providerIdSchema = Schema.String.pipe(Schema.brand("ProviderID"))
@@ -9,7 +9,7 @@ export type ProviderID = typeof providerIdSchema.Type
export const ProviderID = providerIdSchema.pipe(
withStatics((schema: typeof providerIdSchema) => ({
zod: z.string().pipe(z.custom<ProviderID>()),
zod: zod(schema),
// Well-known providers
opencode: schema.make("opencode"),
anthropic: schema.make("anthropic"),
@@ -30,7 +30,7 @@ const modelIdSchema = Schema.String.pipe(Schema.brand("ModelID"))
export type ModelID = typeof modelIdSchema.Type
export const ModelID = modelIdSchema.pipe(
withStatics((_schema: typeof modelIdSchema) => ({
zod: z.string().pipe(z.custom<ModelID>()),
withStatics((schema: typeof modelIdSchema) => ({
zod: zod(schema),
})),
)
+11 -5
View File
@@ -408,9 +408,8 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
id.includes("deepseek") ||
id.includes("minimax") ||
id.includes("glm") ||
id.includes("mistral") ||
id.includes("kimi") ||
id.includes("k2p5") ||
id.includes("k2p") ||
id.includes("qwen") ||
id.includes("big-pickle")
)
@@ -713,7 +712,14 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
case "@ai-sdk/mistral":
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
return {}
// https://docs.mistral.ai/capabilities/reasoning/adjustable
if (!model.capabilities.reasoning) return {}
// Only Mistral Small 4 supports reasoning (mistral-small-2603, mistral-small-latest)
const mistralId = model.api.id.toLowerCase()
if (!mistralId.includes("mistral-small-2603") && !mistralId.includes("mistral-small-latest")) return {}
return {
high: { reasoningEffort: "high" },
}
case "@ai-sdk/cohere":
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/cohere
@@ -851,11 +857,11 @@ export function options(input: {
}
}
// Enable thinking by default for kimi-k2.5/k2p5 models using anthropic SDK
// Enable thinking by default for kimi models using anthropic SDK
const modelId = input.model.api.id.toLowerCase()
if (
(input.model.api.npm === "@ai-sdk/anthropic" || input.model.api.npm === "@ai-sdk/google-vertex/anthropic") &&
(modelId.includes("k2p5") || modelId.includes("kimi-k2.5") || modelId.includes("kimi-k2p5"))
(modelId.includes("k2p") || modelId.includes("kimi-k2.") || modelId.includes("kimi-k2p"))
) {
result["thinking"] = {
type: "enabled",
+2 -3
View File
@@ -1,8 +1,7 @@
import { Schema } from "effect"
import z from "zod"
import { Identifier } from "@/id/id"
import { ZodOverride } from "@/util/effect-zod"
import { zod, ZodOverride } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const ptyIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("pty") }).pipe(Schema.brand("PtyID"))
@@ -12,6 +11,6 @@ export type PtyID = typeof ptyIdSchema.Type
export const PtyID = ptyIdSchema.pipe(
withStatics((schema: typeof ptyIdSchema) => ({
ascending: (id?: string) => schema.make(Identifier.ascending("pty", id)),
zod: Identifier.schema("pty").pipe(z.custom<PtyID>()),
zod: zod(schema),
})),
)
+2 -3
View File
@@ -1,8 +1,7 @@
import { Schema } from "effect"
import z from "zod"
import { Identifier } from "@/id/id"
import { ZodOverride } from "@/util/effect-zod"
import { zod, ZodOverride } from "@/util/effect-zod"
import { Newtype } from "@/util/schema"
export class QuestionID extends Newtype<QuestionID>()(
@@ -13,5 +12,5 @@ export class QuestionID extends Newtype<QuestionID>()(
return this.make(Identifier.ascending("question", id))
}
static readonly zod = Identifier.schema("question") as unknown as z.ZodType<QuestionID>
static readonly zod = zod(this)
}
@@ -335,7 +335,7 @@ export const ExperimentalRoutes = lazy(() =>
description: "List of sessions",
content: {
"application/json": {
schema: resolver(Session.GlobalInfo.array()),
schema: resolver(Session.GlobalInfo.zod.array()),
},
},
},
@@ -90,7 +90,7 @@ export const FileRoutes = lazy(() =>
description: "Symbols",
content: {
"application/json": {
schema: resolver(LSP.Symbol.array()),
schema: resolver(LSP.Symbol.zod.array()),
},
},
},
@@ -260,7 +260,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
description: "LSP server status",
content: {
"application/json": {
schema: resolver(LSP.Status.array()),
schema: resolver(LSP.Status.zod.array()),
},
},
},
@@ -23,6 +23,7 @@ import { PermissionID } from "@/permission/schema"
import { ModelID, ProviderID } from "@/provider/schema"
import { errors } from "../../error"
import { lazy } from "@/util/lazy"
import { zodObject } from "@/util/effect-zod"
import { Bus } from "@/bus"
import { NamedError } from "@opencode-ai/shared/util/error"
import { jsonRequest, runRequest } from "./trace"
@@ -42,7 +43,7 @@ export const SessionRoutes = lazy(() =>
description: "List of sessions",
content: {
"application/json": {
schema: resolver(Session.Info.array()),
schema: resolver(Session.Info.zod.array()),
},
},
},
@@ -87,7 +88,7 @@ export const SessionRoutes = lazy(() =>
description: "Get session status",
content: {
"application/json": {
schema: resolver(z.record(z.string(), SessionStatus.Info)),
schema: resolver(z.record(z.string(), SessionStatus.Info.zod)),
},
},
},
@@ -112,7 +113,7 @@ export const SessionRoutes = lazy(() =>
description: "Get session",
content: {
"application/json": {
schema: resolver(Session.Info),
schema: resolver(Session.Info.zod),
},
},
},
@@ -122,7 +123,7 @@ export const SessionRoutes = lazy(() =>
validator(
"param",
z.object({
sessionID: Session.GetInput,
sessionID: Session.GetInput.zod,
}),
),
async (c) => {
@@ -145,7 +146,7 @@ export const SessionRoutes = lazy(() =>
description: "List of children",
content: {
"application/json": {
schema: resolver(Session.Info.array()),
schema: resolver(Session.Info.zod.array()),
},
},
},
@@ -155,7 +156,7 @@ export const SessionRoutes = lazy(() =>
validator(
"param",
z.object({
sessionID: Session.ChildrenInput,
sessionID: Session.ChildrenInput.zod,
}),
),
async (c) => {
@@ -177,7 +178,7 @@ export const SessionRoutes = lazy(() =>
description: "Todo list",
content: {
"application/json": {
schema: resolver(Todo.Info.array()),
schema: resolver(Todo.Info.zod.array()),
},
},
},
@@ -210,13 +211,13 @@ export const SessionRoutes = lazy(() =>
description: "Successfully created session",
content: {
"application/json": {
schema: resolver(Session.Info),
schema: resolver(Session.Info.zod),
},
},
},
},
}),
validator("json", Session.CreateInput),
validator("json", Session.CreateInput.zod),
async (c) =>
jsonRequest("SessionRoutes.create", c, function* () {
const body = c.req.valid("json") ?? {}
@@ -245,7 +246,7 @@ export const SessionRoutes = lazy(() =>
validator(
"param",
z.object({
sessionID: Session.RemoveInput,
sessionID: Session.RemoveInput.zod,
}),
),
async (c) =>
@@ -267,7 +268,7 @@ export const SessionRoutes = lazy(() =>
description: "Successfully updated session",
content: {
"application/json": {
schema: resolver(Session.Info),
schema: resolver(Session.Info.zod),
},
},
},
@@ -375,7 +376,7 @@ export const SessionRoutes = lazy(() =>
description: "200",
content: {
"application/json": {
schema: resolver(Session.Info),
schema: resolver(Session.Info.zod),
},
},
},
@@ -384,14 +385,14 @@ export const SessionRoutes = lazy(() =>
validator(
"param",
z.object({
sessionID: Session.ForkInput.shape.sessionID,
sessionID: SessionID.zod,
}),
),
validator("json", Session.ForkInput.omit({ sessionID: true })),
validator("json", zodObject(Session.ForkInput).omit({ sessionID: true })),
async (c) =>
jsonRequest("SessionRoutes.fork", c, function* () {
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")
const body = c.req.valid("json") as { messageID?: MessageID }
const svc = yield* Session.Service
return yield* svc.fork({ ...body, sessionID })
}),
@@ -438,7 +439,7 @@ export const SessionRoutes = lazy(() =>
description: "Successfully shared session",
content: {
"application/json": {
schema: resolver(Session.Info),
schema: resolver(Session.Info.zod),
},
},
},
@@ -471,7 +472,7 @@ export const SessionRoutes = lazy(() =>
description: "Successfully retrieved diff",
content: {
"application/json": {
schema: resolver(Snapshot.FileDiff.array()),
schema: resolver(Snapshot.FileDiff.zod.array()),
},
},
},
@@ -480,18 +481,13 @@ export const SessionRoutes = lazy(() =>
validator(
"param",
z.object({
sessionID: SessionSummary.DiffInput.shape.sessionID,
}),
),
validator(
"query",
z.object({
messageID: SessionSummary.DiffInput.shape.messageID,
sessionID: SessionID.zod,
}),
),
validator("query", zodObject(SessionSummary.DiffInput).omit({ sessionID: true })),
async (c) =>
jsonRequest("SessionRoutes.diff", c, function* () {
const query = c.req.valid("query")
const query = c.req.valid("query") as Omit<SessionSummary.DiffInput, "sessionID">
const params = c.req.valid("param")
const summary = yield* SessionSummary.Service
return yield* summary.diff({
@@ -511,7 +507,7 @@ export const SessionRoutes = lazy(() =>
description: "Successfully unshared session",
content: {
"application/json": {
schema: resolver(Session.Info),
schema: resolver(Session.Info.zod),
},
},
},
@@ -611,7 +607,7 @@ export const SessionRoutes = lazy(() =>
description: "List of messages",
content: {
"application/json": {
schema: resolver(MessageV2.WithParts.array()),
schema: resolver(MessageV2.WithParts.zod.array()),
},
},
},
@@ -701,8 +697,8 @@ export const SessionRoutes = lazy(() =>
"application/json": {
schema: resolver(
z.object({
info: MessageV2.Info,
parts: MessageV2.Part.array(),
info: MessageV2.Info.zod,
parts: MessageV2.Part.zod.array(),
}),
),
},
@@ -813,7 +809,7 @@ export const SessionRoutes = lazy(() =>
description: "Successfully updated part",
content: {
"application/json": {
schema: resolver(MessageV2.Part),
schema: resolver(MessageV2.Part.zod),
},
},
},
@@ -828,7 +824,7 @@ export const SessionRoutes = lazy(() =>
partID: PartID.zod,
}),
),
validator("json", MessageV2.Part),
validator("json", MessageV2.Part.zod),
async (c) => {
const params = c.req.valid("param")
const body = c.req.valid("json")
@@ -856,8 +852,8 @@ export const SessionRoutes = lazy(() =>
"application/json": {
schema: resolver(
z.object({
info: MessageV2.Assistant,
parts: MessageV2.Part.array(),
info: MessageV2.Assistant.zod,
parts: MessageV2.Part.zod.array(),
}),
),
},
@@ -872,7 +868,7 @@ export const SessionRoutes = lazy(() =>
sessionID: SessionID.zod,
}),
),
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
validator("json", zodObject(SessionPrompt.PromptInput).omit({ sessionID: true })),
async (c) => {
c.status(200)
c.header("Content-Type", "application/json")
@@ -882,7 +878,9 @@ export const SessionRoutes = lazy(() =>
const msg = await runRequest(
"SessionRoutes.prompt",
c,
SessionPrompt.Service.use((svc) => svc.prompt({ ...body, sessionID })),
SessionPrompt.Service.use((svc) =>
svc.prompt({ ...body, sessionID } as unknown as SessionPrompt.PromptInput),
),
)
void stream.write(JSON.stringify(msg))
})
@@ -908,14 +906,16 @@ export const SessionRoutes = lazy(() =>
sessionID: SessionID.zod,
}),
),
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
validator("json", zodObject(SessionPrompt.PromptInput).omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")
void runRequest(
"SessionRoutes.prompt_async",
c,
SessionPrompt.Service.use((svc) => svc.prompt({ ...body, sessionID })),
SessionPrompt.Service.use((svc) =>
svc.prompt({ ...body, sessionID } as unknown as SessionPrompt.PromptInput),
),
).catch((err) => {
log.error("prompt_async failed", { sessionID, error: err })
void Bus.publish(Session.Event.Error, {
@@ -940,8 +940,8 @@ export const SessionRoutes = lazy(() =>
"application/json": {
schema: resolver(
z.object({
info: MessageV2.Assistant,
parts: MessageV2.Part.array(),
info: MessageV2.Assistant.zod,
parts: MessageV2.Part.zod.array(),
}),
),
},
@@ -956,11 +956,11 @@ export const SessionRoutes = lazy(() =>
sessionID: SessionID.zod,
}),
),
validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })),
validator("json", zodObject(SessionPrompt.CommandInput).omit({ sessionID: true })),
async (c) =>
jsonRequest("SessionRoutes.command", c, function* () {
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")
const body = c.req.valid("json") as Omit<SessionPrompt.CommandInput, "sessionID">
const svc = yield* SessionPrompt.Service
return yield* svc.command({ ...body, sessionID })
}),
@@ -976,7 +976,7 @@ export const SessionRoutes = lazy(() =>
description: "Created message",
content: {
"application/json": {
schema: resolver(MessageV2.WithParts),
schema: resolver(MessageV2.WithParts.zod),
},
},
},
@@ -989,11 +989,11 @@ export const SessionRoutes = lazy(() =>
sessionID: SessionID.zod,
}),
),
validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })),
validator("json", zodObject(SessionPrompt.ShellInput).omit({ sessionID: true })),
async (c) =>
jsonRequest("SessionRoutes.shell", c, function* () {
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")
const body = c.req.valid("json") as Omit<SessionPrompt.ShellInput, "sessionID">
const svc = yield* SessionPrompt.Service
return yield* svc.shell({ ...body, sessionID })
}),
@@ -1009,7 +1009,7 @@ export const SessionRoutes = lazy(() =>
description: "Updated session",
content: {
"application/json": {
schema: resolver(Session.Info),
schema: resolver(Session.Info.zod),
},
},
},
@@ -1022,16 +1022,14 @@ export const SessionRoutes = lazy(() =>
sessionID: SessionID.zod,
}),
),
validator("json", SessionRevert.RevertInput.omit({ sessionID: true })),
validator("json", zodObject(SessionRevert.RevertInput).omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").sessionID
log.info("revert", c.req.valid("json"))
const body = c.req.valid("json") as Omit<SessionRevert.RevertInput, "sessionID">
log.info("revert", body)
return jsonRequest("SessionRoutes.revert", c, function* () {
const svc = yield* SessionRevert.Service
return yield* svc.revert({
sessionID,
...c.req.valid("json"),
})
return yield* svc.revert({ sessionID, ...body })
})
},
)
@@ -1046,7 +1044,7 @@ export const SessionRoutes = lazy(() =>
description: "Updated session",
content: {
"application/json": {
schema: resolver(Session.Info),
schema: resolver(Session.Info.zod),
},
},
},
+151 -47
View File
@@ -32,16 +32,105 @@ export const Event = {
export const PRUNE_MINIMUM = 20_000
export const PRUNE_PROTECT = 40_000
const TOOL_OUTPUT_MAX_CHARS = 2_000
const PRUNE_PROTECTED_TOOLS = ["skill"]
const DEFAULT_TAIL_TURNS = 2
const MIN_PRESERVE_RECENT_TOKENS = 2_000
const MAX_PRESERVE_RECENT_TOKENS = 8_000
const SUMMARY_TEMPLATE = `Output exactly this Markdown structure and keep the section order unchanged:
---
## Goal
- [single-sentence task summary]
## Constraints & Preferences
- [user constraints, preferences, specs, or "(none)"]
## Progress
### Done
- [completed work or "(none)"]
### In Progress
- [current work or "(none)"]
### Blocked
- [blockers or "(none)"]
## Key Decisions
- [decision and why, or "(none)"]
## Next Steps
- [ordered next actions or "(none)"]
## Critical Context
- [important technical facts, errors, open questions, or "(none)"]
## Relevant Files
- [file or directory path: why it matters, or "(none)"]
---
Rules:
- Keep every section, even when empty.
- Use terse bullets, not prose paragraphs.
- Preserve exact file paths, commands, error strings, and identifiers when known.
- Do not mention the summary process or that context was compacted.`
type Turn = {
start: number
end: number
id: MessageID
}
type Tail = {
start: number
id: MessageID
}
type CompletedCompaction = {
userIndex: number
assistantIndex: number
summary: string | undefined
}
function summaryText(message: MessageV2.WithParts) {
const text = message.parts
.filter((part): part is MessageV2.TextPart => part.type === "text")
.map((part) => part.text.trim())
.filter(Boolean)
.join("\n\n")
.trim()
return text || undefined
}
function completedCompactions(messages: MessageV2.WithParts[]) {
const users = new Map<MessageID, number>()
for (let i = 0; i < messages.length; i++) {
const msg = messages[i]
if (msg.info.role !== "user") continue
if (!msg.parts.some((part) => part.type === "compaction")) continue
users.set(msg.info.id, i)
}
return messages.flatMap((msg, assistantIndex): CompletedCompaction[] => {
if (msg.info.role !== "assistant") return []
if (!msg.info.summary || !msg.info.finish || msg.info.error) return []
const userIndex = users.get(msg.info.parentID)
if (userIndex === undefined) return []
return [{ userIndex, assistantIndex, summary: summaryText(msg) }]
})
}
function buildPrompt(input: { previousSummary?: string; context: string[] }) {
const anchor = input.previousSummary
? [
"Update the anchored summary below using the conversation history above.",
"Preserve still-true details, remove stale details, and merge in the new facts.",
"<previous-summary>",
input.previousSummary,
"</previous-summary>",
].join("\n")
: "Create a new anchored summary from the conversation history above."
return [anchor, SUMMARY_TEMPLATE, ...input.context].join("\n\n")
}
function preserveRecentBudget(input: { cfg: Config.Info; model: Provider.Model }) {
return (
input.cfg.compaction?.preserve_recent_tokens ??
@@ -67,6 +156,31 @@ function turns(messages: MessageV2.WithParts[]) {
return result
}
function splitTurn(input: {
messages: MessageV2.WithParts[]
turn: Turn
model: Provider.Model
budget: number
estimate: (input: { messages: MessageV2.WithParts[]; model: Provider.Model }) => Effect.Effect<number>
}) {
return Effect.gen(function* () {
if (input.budget <= 0) return undefined
if (input.turn.end - input.turn.start <= 1) return undefined
for (let start = input.turn.start + 1; start < input.turn.end; start++) {
const size = yield* input.estimate({
messages: input.messages.slice(start, input.turn.end),
model: input.model,
})
if (size > input.budget) continue
return {
start,
id: input.messages[start]!.info.id,
} satisfies Tail
}
return undefined
})
}
export interface Interface {
readonly isOverflow: (input: {
tokens: MessageV2.Assistant["tokens"]
@@ -147,18 +261,28 @@ export const layer: Layer.Layer<
}),
{ concurrency: 1 },
)
if (sizes.at(-1)! > budget) {
log.info("tail fallback", { budget, size: sizes.at(-1) })
return { head: input.messages, tail_start_id: undefined }
}
let total = 0
let keep: Turn | undefined
let keep: Tail | undefined
for (let i = recent.length - 1; i >= 0; i--) {
const turn = recent[i]!
const size = sizes[i]
if (total + size > budget) break
total += size
keep = recent[i]
if (total + size <= budget) {
total += size
keep = { start: turn.start, id: turn.id }
continue
}
const remaining = budget - total
const split = yield* splitTurn({
messages: input.messages,
turn,
model: input.model,
budget: remaining,
estimate,
})
if (split) keep = split
else if (!keep) log.info("tail fallback", { budget, size, total })
break
}
if (!keep || keep.start === 0) return { head: input.messages, tail_start_id: undefined }
@@ -192,17 +316,15 @@ export const layer: Layer.Layer<
if (msg.info.role === "assistant" && msg.info.summary) break loop
for (let partIndex = msg.parts.length - 1; partIndex >= 0; partIndex--) {
const part = msg.parts[partIndex]
if (part.type === "tool")
if (part.state.status === "completed") {
if (PRUNE_PROTECTED_TOOLS.includes(part.tool)) continue
if (part.state.time.compacted) break loop
const estimate = Token.estimate(part.state.output)
total += estimate
if (total > PRUNE_PROTECT) {
pruned += estimate
toPrune.push(part)
}
}
if (part.type !== "tool") continue
if (part.state.status !== "completed") continue
if (PRUNE_PROTECTED_TOOLS.includes(part.tool)) continue
if (part.state.time.compacted) break loop
const estimate = Token.estimate(part.state.output)
total += estimate
if (total <= PRUNE_PROTECT) continue
pruned += estimate
toPrune.push(part)
}
}
@@ -263,8 +385,11 @@ export const layer: Layer.Layer<
: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID)
const cfg = yield* config.get()
const history = compactionPart && messages.at(-1)?.info.id === input.parentID ? messages.slice(0, -1) : messages
const prior = completedCompactions(history)
const hidden = new Set(prior.flatMap((item) => [item.userIndex, item.assistantIndex]))
const previousSummary = prior.at(-1)?.summary
const selected = yield* select({
messages: history,
messages: history.filter((_, index) => !hidden.has(index)),
cfg,
model,
})
@@ -274,34 +399,13 @@ export const layer: Layer.Layer<
{ sessionID: input.sessionID },
{ context: [], prompt: undefined },
)
const defaultPrompt = `When constructing the summary, try to stick to this template:
---
## Goal
[What goal(s) is the user trying to accomplish?]
## Instructions
- [What important instructions did the user give you that are relevant]
- [If there is a plan or spec, include information about it so next agent can continue using it]
## Discoveries
[What notable things were learned during this conversation that would be useful for the next agent to know when continuing the work]
## Accomplished
[What work has been completed, what work is still in progress, and what work is left?]
## Relevant files / directories
[Construct a structured list of relevant files that have been read, edited, or created that pertain to the task at hand. If all the files in a directory are relevant, include the path to the directory.]
---`
const prompt = compacting.prompt ?? [defaultPrompt, ...compacting.context].join("\n\n")
const nextPrompt = compacting.prompt ?? buildPrompt({ previousSummary, context: compacting.context })
const msgs = structuredClone(selected.head)
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, { stripMedia: true })
const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, {
stripMedia: true,
toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
})
const ctx = yield* InstanceState.context
const msg: MessageV2.Assistant = {
id: MessageID.ascending(),
@@ -345,7 +449,7 @@ export const layer: Layer.Layer<
...modelMessages,
{
role: "user",
content: [{ type: "text", text: prompt }],
content: [{ type: "text", text: nextPrompt }],
},
],
model,
File diff suppressed because it is too large Load Diff
+177 -176
View File
@@ -1,191 +1,192 @@
import z from "zod"
import { Schema } from "effect"
import { SessionID } from "./schema"
import { ModelID, ProviderID } from "../provider/schema"
import { NamedError } from "@opencode-ai/shared/util/error"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import { namedSchemaError } from "@/util/named-schema-error"
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
export const AuthError = NamedError.create(
"ProviderAuthError",
z.object({
providerID: z.string(),
message: z.string(),
}),
)
export const ToolCall = z
.object({
state: z.literal("call"),
step: z.number().optional(),
toolCallId: z.string(),
toolName: z.string(),
args: z.custom<Required<unknown>>(),
})
.meta({
ref: "ToolCall",
})
export type ToolCall = z.infer<typeof ToolCall>
export const ToolPartialCall = z
.object({
state: z.literal("partial-call"),
step: z.number().optional(),
toolCallId: z.string(),
toolName: z.string(),
args: z.custom<Required<unknown>>(),
})
.meta({
ref: "ToolPartialCall",
})
export type ToolPartialCall = z.infer<typeof ToolPartialCall>
export const ToolResult = z
.object({
state: z.literal("result"),
step: z.number().optional(),
toolCallId: z.string(),
toolName: z.string(),
args: z.custom<Required<unknown>>(),
result: z.string(),
})
.meta({
ref: "ToolResult",
})
export type ToolResult = z.infer<typeof ToolResult>
export const ToolInvocation = z.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]).meta({
ref: "ToolInvocation",
export const OutputLengthError = namedSchemaError("MessageOutputLengthError", {})
export const AuthError = namedSchemaError("ProviderAuthError", {
providerID: Schema.String,
message: Schema.String,
})
export type ToolInvocation = z.infer<typeof ToolInvocation>
export const TextPart = z
.object({
type: z.literal("text"),
text: z.string(),
})
.meta({
ref: "TextPart",
})
export type TextPart = z.infer<typeof TextPart>
const AuthErrorEffect = Schema.Struct({
name: Schema.Literal("ProviderAuthError"),
data: Schema.Struct({
providerID: Schema.String,
message: Schema.String,
}),
})
export const ReasoningPart = z
.object({
type: z.literal("reasoning"),
text: z.string(),
providerMetadata: z.record(z.string(), z.any()).optional(),
})
.meta({
ref: "ReasoningPart",
})
export type ReasoningPart = z.infer<typeof ReasoningPart>
const OutputLengthErrorEffect = Schema.Struct({
name: Schema.Literal("MessageOutputLengthError"),
data: Schema.Struct({}),
})
export const ToolInvocationPart = z
.object({
type: z.literal("tool-invocation"),
toolInvocation: ToolInvocation,
})
.meta({
ref: "ToolInvocationPart",
})
export type ToolInvocationPart = z.infer<typeof ToolInvocationPart>
const UnknownErrorEffect = Schema.Struct({
name: Schema.Literal("UnknownError"),
data: Schema.Struct({
message: Schema.String,
}),
})
export const SourceUrlPart = z
.object({
type: z.literal("source-url"),
sourceId: z.string(),
url: z.string(),
title: z.string().optional(),
providerMetadata: z.record(z.string(), z.any()).optional(),
})
.meta({
ref: "SourceUrlPart",
})
export type SourceUrlPart = z.infer<typeof SourceUrlPart>
export const ToolCall = Schema.Struct({
state: Schema.Literal("call"),
step: Schema.optional(Schema.Number),
toolCallId: Schema.String,
toolName: Schema.String,
args: Schema.Unknown,
})
.annotate({ identifier: "ToolCall" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type ToolCall = Schema.Schema.Type<typeof ToolCall>
export const FilePart = z
.object({
type: z.literal("file"),
mediaType: z.string(),
filename: z.string().optional(),
url: z.string(),
})
.meta({
ref: "FilePart",
})
export type FilePart = z.infer<typeof FilePart>
export const ToolPartialCall = Schema.Struct({
state: Schema.Literal("partial-call"),
step: Schema.optional(Schema.Number),
toolCallId: Schema.String,
toolName: Schema.String,
args: Schema.Unknown,
})
.annotate({ identifier: "ToolPartialCall" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type ToolPartialCall = Schema.Schema.Type<typeof ToolPartialCall>
export const StepStartPart = z
.object({
type: z.literal("step-start"),
})
.meta({
ref: "StepStartPart",
})
export type StepStartPart = z.infer<typeof StepStartPart>
export const ToolResult = Schema.Struct({
state: Schema.Literal("result"),
step: Schema.optional(Schema.Number),
toolCallId: Schema.String,
toolName: Schema.String,
args: Schema.Unknown,
result: Schema.String,
})
.annotate({ identifier: "ToolResult" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type ToolResult = Schema.Schema.Type<typeof ToolResult>
export const MessagePart = z
.discriminatedUnion("type", [TextPart, ReasoningPart, ToolInvocationPart, SourceUrlPart, FilePart, StepStartPart])
.meta({
ref: "MessagePart",
})
export type MessagePart = z.infer<typeof MessagePart>
export const ToolInvocation = Schema.Union([ToolCall, ToolPartialCall, ToolResult])
.annotate({ identifier: "ToolInvocation", discriminator: "state" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type ToolInvocation = Schema.Schema.Type<typeof ToolInvocation>
export const Info = z
.object({
id: z.string(),
role: z.enum(["user", "assistant"]),
parts: z.array(MessagePart),
metadata: z
.object({
time: z.object({
created: z.number(),
completed: z.number().optional(),
export const TextPart = Schema.Struct({
type: Schema.Literal("text"),
text: Schema.String,
})
.annotate({ identifier: "TextPart" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type TextPart = Schema.Schema.Type<typeof TextPart>
export const ReasoningPart = Schema.Struct({
type: Schema.Literal("reasoning"),
text: Schema.String,
providerMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
})
.annotate({ identifier: "ReasoningPart" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type ReasoningPart = Schema.Schema.Type<typeof ReasoningPart>
export const ToolInvocationPart = Schema.Struct({
type: Schema.Literal("tool-invocation"),
toolInvocation: ToolInvocation,
})
.annotate({ identifier: "ToolInvocationPart" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type ToolInvocationPart = Schema.Schema.Type<typeof ToolInvocationPart>
export const SourceUrlPart = Schema.Struct({
type: Schema.Literal("source-url"),
sourceId: Schema.String,
url: Schema.String,
title: Schema.optional(Schema.String),
providerMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
})
.annotate({ identifier: "SourceUrlPart" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type SourceUrlPart = Schema.Schema.Type<typeof SourceUrlPart>
export const FilePart = Schema.Struct({
type: Schema.Literal("file"),
mediaType: Schema.String,
filename: Schema.optional(Schema.String),
url: Schema.String,
})
.annotate({ identifier: "FilePart" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type FilePart = Schema.Schema.Type<typeof FilePart>
export const StepStartPart = Schema.Struct({
type: Schema.Literal("step-start"),
})
.annotate({ identifier: "StepStartPart" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type StepStartPart = Schema.Schema.Type<typeof StepStartPart>
export const MessagePart = Schema.Union([
TextPart,
ReasoningPart,
ToolInvocationPart,
SourceUrlPart,
FilePart,
StepStartPart,
])
.annotate({ identifier: "MessagePart", discriminator: "type" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type MessagePart = Schema.Schema.Type<typeof MessagePart>
export const Info = Schema.Struct({
id: Schema.String,
role: Schema.Literals(["user", "assistant"]),
parts: Schema.Array(MessagePart),
metadata: Schema.Struct({
time: Schema.Struct({
created: Schema.Number,
completed: Schema.optional(Schema.Number),
}),
error: Schema.optional(Schema.Union([AuthErrorEffect, UnknownErrorEffect, OutputLengthErrorEffect])),
sessionID: SessionID,
tool: Schema.Record(
Schema.String,
Schema.StructWithRest(
Schema.Struct({
title: Schema.String,
snapshot: Schema.optional(Schema.String),
time: Schema.Struct({
start: Schema.Number,
end: Schema.Number,
}),
}),
error: z
.discriminatedUnion("name", [AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema])
.optional(),
sessionID: SessionID.zod,
tool: z.record(
z.string(),
z
.object({
title: z.string(),
snapshot: z.string().optional(),
time: z.object({
start: z.number(),
end: z.number(),
}),
})
.catchall(z.any()),
),
assistant: z
.object({
system: z.string().array(),
modelID: ModelID.zod,
providerID: ProviderID.zod,
path: z.object({
cwd: z.string(),
root: z.string(),
}),
cost: z.number(),
summary: z.boolean().optional(),
tokens: z.object({
input: z.number(),
output: z.number(),
reasoning: z.number(),
cache: z.object({
read: z.number(),
write: z.number(),
}),
}),
})
.optional(),
snapshot: z.string().optional(),
})
.meta({ ref: "MessageMetadata" }),
})
.meta({
ref: "Message",
})
export type Info = z.infer<typeof Info>
[Schema.Record(Schema.String, Schema.Unknown)],
),
),
assistant: Schema.optional(
Schema.Struct({
system: Schema.Array(Schema.String),
modelID: ModelID,
providerID: ProviderID,
path: Schema.Struct({
cwd: Schema.String,
root: Schema.String,
}),
cost: Schema.Number,
summary: Schema.optional(Schema.Boolean),
tokens: Schema.Struct({
input: Schema.Number,
output: Schema.Number,
reasoning: Schema.Number,
cache: Schema.Struct({
read: Schema.Number,
write: Schema.Number,
}),
}),
}),
),
snapshot: Schema.optional(Schema.String),
}).annotate({ identifier: "MessageMetadata" }),
})
.annotate({ identifier: "Message" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type Info = Schema.Schema.Type<typeof Info>
export * as Message from "./message"
+3 -1
View File
@@ -62,7 +62,9 @@ export function toPartialRow(info: DeepPartial<Session.Info>) {
export default [
SyncEvent.project(Session.Event.Created, (db, data) => {
db.insert(SessionTable).values(Session.toRow(data.info)).run()
db.insert(SessionTable)
.values(Session.toRow(data.info as Session.Info))
.run()
}),
SyncEvent.project(Session.Event.Updated, (db, data) => {
+86 -107
View File
@@ -43,7 +43,9 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Truncate } from "@/tool"
import { decodeDataUrl } from "@/util/data-url"
import { Process } from "@/util"
import { Cause, Effect, Exit, Layer, Option, Scope, Context } from "effect"
import { Cause, Effect, Exit, Layer, Option, Scope, Context, Schema } from "effect"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import { EffectLogger } from "@/effect"
import { InstanceState } from "@/effect"
import { TaskTool, type TaskPromptOps } from "@/tool/task"
@@ -69,7 +71,7 @@ const elog = EffectLogger.create({ service: "session.prompt" })
export interface Interface {
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
readonly prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts>
readonly loop: (input: z.infer<typeof LoopInput>) => Effect.Effect<MessageV2.WithParts>
readonly loop: (input: LoopInput) => Effect.Effect<MessageV2.WithParts>
readonly shell: (input: ShellInput) => Effect.Effect<MessageV2.WithParts>
readonly command: (input: CommandInput) => Effect.Effect<MessageV2.WithParts>
readonly resolvePromptParts: (template: string) => Effect.Effect<PromptInput["parts"]>
@@ -1243,7 +1245,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
{ message: info, parts },
)
const parsed = MessageV2.Info.safeParse(info)
const parsed = MessageV2.Info.zod.safeParse(info)
if (!parsed.success) {
log.error("invalid user message before save", {
sessionID: input.sessionID,
@@ -1254,7 +1256,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
})
}
parts.forEach((part, index) => {
const p = MessageV2.Part.safeParse(part)
const p = MessageV2.Part.zod.safeParse(part)
if (p.success) return
log.error("invalid user part before save", {
sessionID: input.sessionID,
@@ -1532,9 +1534,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
},
)
const loop: (input: z.infer<typeof LoopInput>) => Effect.Effect<MessageV2.WithParts> = Effect.fn(
"SessionPrompt.loop",
)(function* (input: z.infer<typeof LoopInput>) {
const loop: (input: LoopInput) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.loop")(function* (
input: LoopInput,
) {
return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID))
})
@@ -1701,111 +1703,88 @@ export const defaultLayer = Layer.suspend(() =>
),
),
)
export const PromptInput = z.object({
sessionID: SessionID.zod,
messageID: MessageID.zod.optional(),
model: z
.object({
providerID: ProviderID.zod,
modelID: ModelID.zod,
})
.optional(),
agent: z.string().optional(),
noReply: z.boolean().optional(),
tools: z
.record(z.string(), z.boolean())
.optional()
.describe("@deprecated tools and permissions have been merged, you can set permissions on the session itself now"),
format: MessageV2.Format.optional(),
system: z.string().optional(),
variant: z.string().optional(),
parts: z.array(
z.discriminatedUnion("type", [
MessageV2.TextPart.omit({
messageID: true,
sessionID: true,
})
.partial({
id: true,
})
.meta({
ref: "TextPartInput",
}),
MessageV2.FilePart.omit({
messageID: true,
sessionID: true,
})
.partial({
id: true,
})
.meta({
ref: "FilePartInput",
}),
MessageV2.AgentPart.omit({
messageID: true,
sessionID: true,
})
.partial({
id: true,
})
.meta({
ref: "AgentPartInput",
}),
MessageV2.SubtaskPart.omit({
messageID: true,
sessionID: true,
})
.partial({
id: true,
})
.meta({
ref: "SubtaskPartInput",
}),
]),
const ModelRef = Schema.Struct({
providerID: ProviderID,
modelID: ModelID,
})
export const PromptInput = Schema.Struct({
sessionID: SessionID,
messageID: Schema.optional(MessageID),
model: Schema.optional(ModelRef),
agent: Schema.optional(Schema.String),
noReply: Schema.optional(Schema.Boolean),
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({
description:
"@deprecated tools and permissions have been merged, you can set permissions on the session itself now",
}),
format: Schema.optional(MessageV2.Format),
system: Schema.optional(Schema.String),
variant: Schema.optional(Schema.String),
parts: Schema.Array(
Schema.Union([
MessageV2.TextPartInput,
MessageV2.FilePartInput,
MessageV2.AgentPartInput,
MessageV2.SubtaskPartInput,
]).annotate({ discriminator: "type" }),
),
})
export type PromptInput = z.infer<typeof PromptInput>
}).pipe(withStatics((s) => ({ zod: zod(s) })))
// `z.discriminatedUnion` erases the discriminated members' shapes back to
// `{}` when walked from the generic `z.ZodType` input. Restore the precise
// `parts` type from the exported Schema input types so callers see a proper
// tagged union.
type PartInputUnion =
| MessageV2.TextPartInput
| MessageV2.FilePartInput
| MessageV2.AgentPartInput
| MessageV2.SubtaskPartInput
export type PromptInput = Omit<Schema.Schema.Type<typeof PromptInput>, "parts"> & {
parts: PartInputUnion[]
}
export const LoopInput = z.object({
sessionID: SessionID.zod,
})
export class LoopInput extends Schema.Class<LoopInput>("SessionPrompt.LoopInput")({
sessionID: SessionID,
}) {
static readonly zod = zod(this)
}
export const ShellInput = z.object({
sessionID: SessionID.zod,
messageID: MessageID.zod.optional(),
agent: z.string(),
model: z
.object({
providerID: ProviderID.zod,
modelID: ModelID.zod,
})
.optional(),
command: z.string(),
})
export type ShellInput = z.infer<typeof ShellInput>
export const ShellInput = Schema.Struct({
sessionID: SessionID,
messageID: Schema.optional(MessageID),
agent: Schema.String,
model: Schema.optional(ModelRef),
command: Schema.String,
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type ShellInput = Schema.Schema.Type<typeof ShellInput>
export const CommandInput = z.object({
messageID: MessageID.zod.optional(),
sessionID: SessionID.zod,
agent: z.string().optional(),
model: z.string().optional(),
arguments: z.string(),
command: z.string(),
variant: z.string().optional(),
parts: z
.array(
z.discriminatedUnion("type", [
MessageV2.FilePart.omit({
messageID: true,
sessionID: true,
}).partial({
id: true,
export const CommandInput = Schema.Struct({
messageID: Schema.optional(MessageID),
sessionID: SessionID,
agent: Schema.optional(Schema.String),
model: Schema.optional(Schema.String),
arguments: Schema.String,
command: Schema.String,
variant: Schema.optional(Schema.String),
// Inlined (no identifier annotation) to keep the original SDK output — the
// PromptInput call site below references FilePartInput by ref via the
// Schema export in message-v2.ts.
parts: Schema.optional(
Schema.Array(
Schema.Union([
Schema.Struct({
id: Schema.optional(PartID),
type: Schema.Literal("file"),
mime: Schema.String,
filename: Schema.optional(Schema.String),
url: Schema.String,
source: Schema.optional(MessageV2.FilePartSource),
}),
]),
)
.optional(),
})
export type CommandInput = z.infer<typeof CommandInput>
]).annotate({ discriminator: "type" }),
),
),
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type CommandInput = Schema.Schema.Type<typeof CommandInput>
/** @internal Exported for testing */
export function createStructuredOutputTool(input: {
+4 -29
View File
@@ -1,7 +1,6 @@
import type { NamedError } from "@opencode-ai/shared/util/error"
import { Cause, Clock, Duration, Effect, Schedule } from "effect"
import { MessageV2 } from "./message-v2"
import { iife } from "@/util/iife"
export type Err = ReturnType<NamedError["toObject"]>
@@ -63,43 +62,19 @@ export function retryable(error: Err) {
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
}
// Check for rate limit patterns in plain text error messages
const msg = error.data?.message
if (typeof msg === "string") {
const lower = msg.toLowerCase()
if (
lower.includes("rate increased too quickly") ||
lower.includes("rate limit") ||
lower.includes("too many requests")
/rate.?limit|too.?many.?requests|overloaded|exhausted|unavailable|service.?unavailable|429|5\d\d|internal.?error|network.?error|connection.?(?:error|refused|lost|reset)|fetch.?failed|upstream|upstream.?connect|reset before headers|socket.?hang.?up|other.?side.?closed|ended without|timed? out|timeout|terminated|retry delay|provider.?returned.?error|server.?error/i.test(
lower,
)
) {
if (lower.includes("overloaded") || lower.includes("exhausted") || lower.includes("unavailable")) return "Provider is overloaded"
return msg
}
}
const json = iife(() => {
try {
if (typeof error.data?.message === "string") {
const parsed = JSON.parse(error.data.message)
return parsed
}
return JSON.parse(error.data.message)
} catch {
return undefined
}
})
if (!json || typeof json !== "object") return undefined
const code = typeof json.code === "string" ? json.code : ""
if (json.type === "error" && json.error?.type === "too_many_requests") {
return "Too Many Requests"
}
if (code.includes("exhausted") || code.includes("unavailable")) {
return "Provider is overloaded"
}
if (json.type === "error" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) {
return "Rate Limited"
}
return undefined
}
+9 -8
View File
@@ -1,10 +1,11 @@
import z from "zod"
import { Effect, Layer, Context } from "effect"
import { Effect, Layer, Context, Schema } from "effect"
import { Bus } from "../bus"
import { Snapshot } from "../snapshot"
import { Storage } from "@/storage"
import { SyncEvent } from "../sync"
import { Log } from "../util"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import * as Session from "./session"
import { MessageV2 } from "./message-v2"
import { SessionID, MessageID, PartID } from "./schema"
@@ -13,12 +14,12 @@ import { SessionSummary } from "./summary"
const log = Log.create({ service: "session.revert" })
export const RevertInput = z.object({
sessionID: SessionID.zod,
messageID: MessageID.zod,
partID: PartID.zod.optional(),
})
export type RevertInput = z.infer<typeof RevertInput>
export const RevertInput = Schema.Struct({
sessionID: SessionID,
messageID: MessageID,
partID: Schema.optional(PartID),
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type RevertInput = Schema.Schema.Type<typeof RevertInput>
export interface Interface {
readonly revert: (input: RevertInput) => Effect.Effect<Session.Info>
+4 -5
View File
@@ -1,15 +1,14 @@
import { Schema } from "effect"
import z from "zod"
import { Identifier } from "@/id/id"
import { ZodOverride } from "@/util/effect-zod"
import { zod, ZodOverride } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
export const SessionID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("session") }).pipe(
Schema.brand("SessionID"),
withStatics((s) => ({
descending: (id?: string) => s.make(Identifier.descending("session", id)),
zod: Identifier.schema("session").pipe(z.custom<Schema.Schema.Type<typeof s>>()),
zod: zod(s),
})),
)
@@ -19,7 +18,7 @@ export const MessageID = Schema.String.annotate({ [ZodOverride]: Identifier.sche
Schema.brand("MessageID"),
withStatics((s) => ({
ascending: (id?: string) => s.make(Identifier.ascending("message", id)),
zod: Identifier.schema("message").pipe(z.custom<Schema.Schema.Type<typeof s>>()),
zod: zod(s),
})),
)
@@ -29,7 +28,7 @@ export const PartID = Schema.String.annotate({ [ZodOverride]: Identifier.schema(
Schema.brand("PartID"),
withStatics((s) => ({
ascending: (id?: string) => s.make(Identifier.ascending("part", id)),
zod: Identifier.schema("part").pipe(z.custom<Schema.Schema.Type<typeof s>>()),
zod: zod(s),
})),
)
+106 -91
View File
@@ -27,7 +27,9 @@ import { SessionID, MessageID, PartID } from "./schema"
import type { Provider } from "@/provider"
import { Permission } from "@/permission"
import { Global } from "@/global"
import { Effect, Layer, Option, Context } from "effect"
import { Effect, Layer, Option, Context, Schema, Types } from "effect"
import { zod, zodObject } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const log = Log.create({ service: "session" })
@@ -114,91 +116,104 @@ function getForkedTitle(title: string): string {
return `${title} (fork #1)`
}
export const Info = z
.object({
id: SessionID.zod,
slug: z.string(),
projectID: ProjectID.zod,
workspaceID: WorkspaceID.zod.optional(),
directory: z.string(),
parentID: SessionID.zod.optional(),
summary: z
.object({
additions: z.number(),
deletions: z.number(),
files: z.number(),
diffs: Snapshot.FileDiff.array().optional(),
})
.optional(),
share: z
.object({
url: z.string(),
})
.optional(),
title: z.string(),
version: z.string(),
time: z.object({
created: z.number(),
updated: z.number(),
compacting: z.number().optional(),
archived: z.number().optional(),
}),
permission: Permission.Ruleset.zod.optional(),
revert: z
.object({
messageID: MessageID.zod,
partID: PartID.zod.optional(),
snapshot: z.string().optional(),
diff: z.string().optional(),
})
.optional(),
})
.meta({
ref: "Session",
})
export type Info = z.output<typeof Info>
export const ProjectInfo = z
.object({
id: ProjectID.zod,
name: z.string().optional(),
worktree: z.string(),
})
.meta({
ref: "ProjectSummary",
})
export type ProjectInfo = z.output<typeof ProjectInfo>
export const GlobalInfo = Info.extend({
project: ProjectInfo.nullable(),
}).meta({
ref: "GlobalSession",
const Summary = Schema.Struct({
additions: Schema.Number,
deletions: Schema.Number,
files: Schema.Number,
diffs: Schema.optional(Schema.Array(Snapshot.FileDiff)),
})
export type GlobalInfo = z.output<typeof GlobalInfo>
export const CreateInput = z
.object({
parentID: SessionID.zod.optional(),
title: z.string().optional(),
permission: Info.shape.permission,
workspaceID: WorkspaceID.zod.optional(),
})
.optional()
export type CreateInput = z.output<typeof CreateInput>
export const ForkInput = z.object({ sessionID: SessionID.zod, messageID: MessageID.zod.optional() })
export const GetInput = SessionID.zod
export const ChildrenInput = SessionID.zod
export const RemoveInput = SessionID.zod
export const SetTitleInput = z.object({ sessionID: SessionID.zod, title: z.string() })
export const SetArchivedInput = z.object({ sessionID: SessionID.zod, time: z.number().optional() })
export const SetPermissionInput = z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset.zod })
export const SetRevertInput = z.object({
sessionID: SessionID.zod,
revert: Info.shape.revert,
summary: Info.shape.summary,
const Share = Schema.Struct({
url: Schema.String,
})
export const MessagesInput = z.object({ sessionID: SessionID.zod, limit: z.number().optional() })
const Time = Schema.Struct({
created: Schema.Number,
updated: Schema.Number,
compacting: Schema.optional(Schema.Number),
archived: Schema.optional(Schema.Number),
})
const Revert = Schema.Struct({
messageID: MessageID,
partID: Schema.optional(PartID),
snapshot: Schema.optional(Schema.String),
diff: Schema.optional(Schema.String),
})
export const Info = Schema.Struct({
id: SessionID,
slug: Schema.String,
projectID: ProjectID,
workspaceID: Schema.optional(WorkspaceID),
directory: Schema.String,
parentID: Schema.optional(SessionID),
summary: Schema.optional(Summary),
share: Schema.optional(Share),
title: Schema.String,
version: Schema.String,
time: Time,
permission: Schema.optional(Permission.Ruleset),
revert: Schema.optional(Revert),
})
.annotate({ identifier: "Session" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
export const ProjectInfo = Schema.Struct({
id: ProjectID,
name: Schema.optional(Schema.String),
worktree: Schema.String,
})
.annotate({ identifier: "ProjectSummary" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type ProjectInfo = Types.DeepMutable<Schema.Schema.Type<typeof ProjectInfo>>
export const GlobalInfo = Schema.Struct({
...Info.fields,
project: Schema.NullOr(ProjectInfo),
})
.annotate({ identifier: "GlobalSession" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type GlobalInfo = Types.DeepMutable<Schema.Schema.Type<typeof GlobalInfo>>
export const CreateInput = Schema.optional(
Schema.Struct({
parentID: Schema.optional(SessionID),
title: Schema.optional(Schema.String),
permission: Schema.optional(Permission.Ruleset),
workspaceID: Schema.optional(WorkspaceID),
}),
).pipe(withStatics((s) => ({ zod: zod(s) })))
export type CreateInput = Types.DeepMutable<Schema.Schema.Type<typeof CreateInput>>
export const ForkInput = Schema.Struct({
sessionID: SessionID,
messageID: Schema.optional(MessageID),
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export const GetInput = SessionID
export const ChildrenInput = SessionID
export const RemoveInput = SessionID
export const SetTitleInput = Schema.Struct({ sessionID: SessionID, title: Schema.String }).pipe(
withStatics((s) => ({ zod: zod(s) })),
)
export const SetArchivedInput = Schema.Struct({
sessionID: SessionID,
time: Schema.optional(Schema.Number),
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export const SetPermissionInput = Schema.Struct({
sessionID: SessionID,
permission: Permission.Ruleset,
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export const SetRevertInput = Schema.Struct({
sessionID: SessionID,
revert: Schema.optional(Revert),
summary: Schema.optional(Summary),
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export const MessagesInput = Schema.Struct({
sessionID: SessionID,
limit: Schema.optional(Schema.Number),
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export const Event = {
Created: SyncEvent.define({
@@ -207,7 +222,7 @@ export const Event = {
aggregate: "sessionID",
schema: z.object({
sessionID: SessionID.zod,
info: Info,
info: Info.zod,
}),
}),
Updated: SyncEvent.define({
@@ -216,14 +231,14 @@ export const Event = {
aggregate: "sessionID",
schema: z.object({
sessionID: SessionID.zod,
info: updateSchema(Info).extend({
share: updateSchema(Info.shape.share.unwrap()).optional(),
time: updateSchema(Info.shape.time).optional(),
info: updateSchema(zodObject(Info)).extend({
share: updateSchema(zodObject(Share)).optional(),
time: updateSchema(zodObject(Time)).optional(),
}),
}),
busSchema: z.object({
sessionID: SessionID.zod,
info: Info,
info: Info.zod,
}),
}),
Deleted: SyncEvent.define({
@@ -232,14 +247,14 @@ export const Event = {
aggregate: "sessionID",
schema: z.object({
sessionID: SessionID.zod,
info: Info,
info: Info.zod,
}),
}),
Diff: BusEvent.define(
"session.diff",
z.object({
sessionID: SessionID.zod,
diff: Snapshot.FileDiff.array(),
diff: Snapshot.FileDiff.zod.array(),
}),
),
Error: BusEvent.define(
@@ -247,7 +262,7 @@ export const Event = {
z.object({
sessionID: SessionID.zod.optional(),
// z.lazy defers access to break circular dep: session → message-v2 → provider → plugin → session
error: z.lazy(() => MessageV2.Assistant.shape.error),
error: z.lazy(() => (MessageV2.Assistant.zod as unknown as z.ZodObject<any>).shape.error),
}),
),
}
+21 -21
View File
@@ -2,35 +2,35 @@ import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { InstanceState } from "@/effect"
import { SessionID } from "./schema"
import { Effect, Layer, Context } from "effect"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import { Effect, Layer, Context, Schema } from "effect"
import z from "zod"
export const Info = z
.union([
z.object({
type: z.literal("idle"),
}),
z.object({
type: z.literal("retry"),
attempt: z.number(),
message: z.string(),
next: z.number(),
}),
z.object({
type: z.literal("busy"),
}),
])
.meta({
ref: "SessionStatus",
})
export type Info = z.infer<typeof Info>
export const Info = Schema.Union([
Schema.Struct({
type: Schema.Literal("idle"),
}),
Schema.Struct({
type: Schema.Literal("retry"),
attempt: Schema.Number,
message: Schema.String,
next: Schema.Number,
}),
Schema.Struct({
type: Schema.Literal("busy"),
}),
])
.annotate({ identifier: "SessionStatus" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type Info = Schema.Schema.Type<typeof Info>
export const Event = {
Status: BusEvent.define(
"session.status",
z.object({
sessionID: SessionID.zod,
status: Info,
status: Info.zod,
}),
),
// deprecated
+8 -6
View File
@@ -1,8 +1,9 @@
import z from "zod"
import { Effect, Layer, Context } from "effect"
import { Effect, Layer, Context, Schema } from "effect"
import { Bus } from "@/bus"
import { Snapshot } from "@/snapshot"
import { Storage } from "@/storage"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import * as Session from "./session"
import { MessageV2 } from "./message-v2"
import { SessionID, MessageID } from "./schema"
@@ -155,9 +156,10 @@ export const defaultLayer = Layer.suspend(() =>
),
)
export const DiffInput = z.object({
sessionID: SessionID.zod,
messageID: MessageID.zod.optional(),
})
export const DiffInput = Schema.Struct({
sessionID: SessionID,
messageID: Schema.optional(MessageID),
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type DiffInput = Schema.Schema.Type<typeof DiffInput>
export * as SessionSummary from "./summary"
+14 -10
View File
@@ -1,26 +1,30 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { SessionID } from "./schema"
import { Effect, Layer, Context } from "effect"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
import { Effect, Layer, Context, Schema } from "effect"
import z from "zod"
import { Database, eq, asc } from "../storage"
import { TodoTable } from "./session.sql"
export const Info = z
.object({
content: z.string().describe("Brief description of the task"),
status: z.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
priority: z.string().describe("Priority level of the task: high, medium, low"),
})
.meta({ ref: "Todo" })
export type Info = z.infer<typeof Info>
export const Info = Schema.Struct({
content: Schema.String.annotate({ description: "Brief description of the task" }),
status: Schema.String.annotate({
description: "Current status of the task: pending, in_progress, completed, cancelled",
}),
priority: Schema.String.annotate({ description: "Priority level of the task: high, medium, low" }),
})
.annotate({ identifier: "Todo" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type Info = Schema.Schema.Type<typeof Info>
export const Event = {
Updated: BusEvent.define(
"todo.updated",
z.object({
sessionID: SessionID.zod,
todos: z.array(Info),
todos: z.array(Info.zod),
}),
),
}
+18 -18
View File
@@ -1,4 +1,4 @@
import { Cause, Duration, Effect, Layer, Schedule, Semaphore, Context, Stream } from "effect"
import { Cause, Duration, Effect, Layer, Schedule, Schema, Semaphore, Context, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { formatPatch, structuredPatch } from "diff"
import path from "path"
@@ -10,25 +10,25 @@ import { Hash } from "@opencode-ai/shared/util/hash"
import { Config } from "../config"
import { Global } from "../global"
import { Log } from "../util"
import { withStatics } from "@/util/schema"
import { zod } from "@/util/effect-zod"
export const Patch = z.object({
hash: z.string(),
files: z.string().array(),
export const Patch = Schema.Struct({
hash: Schema.String,
files: Schema.mutable(Schema.Array(Schema.String)),
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type Patch = typeof Patch.Type
export const FileDiff = Schema.Struct({
file: Schema.String,
patch: Schema.String,
additions: Schema.Number,
deletions: Schema.Number,
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
})
export type Patch = z.infer<typeof Patch>
export const FileDiff = z
.object({
file: z.string(),
patch: z.string(),
additions: z.number(),
deletions: z.number(),
status: z.enum(["added", "deleted", "modified"]).optional(),
})
.meta({
ref: "SnapshotFileDiff",
})
export type FileDiff = z.infer<typeof FileDiff>
.annotate({ identifier: "SnapshotFileDiff" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type FileDiff = typeof FileDiff.Type
const log = Log.create({ service: "snapshot" })
const prune = "7.days"
@@ -168,6 +168,7 @@ export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<a
vcs: data.vcs,
name: data.name ?? undefined,
icon_url: data.icon?.url,
icon_url_override: data.icon?.override,
icon_color: data.icon?.color,
time_created: data.time?.created ?? now,
time_updated: data.time?.updated ?? now,
+2 -3
View File
@@ -1,14 +1,13 @@
import { Schema } from "effect"
import z from "zod"
import { Identifier } from "@/id/id"
import { ZodOverride } from "@/util/effect-zod"
import { zod, ZodOverride } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
export const EventID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("event") }).pipe(
Schema.brand("EventID"),
withStatics((s) => ({
ascending: (id?: string) => s.make(Identifier.ascending("event", id)),
zod: Identifier.schema("event").pipe(z.custom<Schema.Schema.Type<typeof s>>()),
zod: zod(s),
})),
)
+28 -18
View File
@@ -14,6 +14,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import DESCRIPTION from "./apply_patch.txt"
import { File } from "../file"
import { Format } from "../format"
import * as Bom from "@/util/bom"
const PatchParams = z.object({
patchText: z.string().describe("The full patch text that describes all changes to be made"),
@@ -59,6 +60,7 @@ export const ApplyPatchTool = Tool.define(
diff: string
additions: number
deletions: number
bom: boolean
}> = []
let totalDiff = ""
@@ -72,11 +74,12 @@ export const ApplyPatchTool = Tool.define(
const oldContent = ""
const newContent =
hunk.contents.length === 0 || hunk.contents.endsWith("\n") ? hunk.contents : `${hunk.contents}\n`
const diff = trimDiff(createTwoFilesPatch(filePath, filePath, oldContent, newContent))
const next = Bom.split(newContent)
const diff = trimDiff(createTwoFilesPatch(filePath, filePath, oldContent, next.text))
let additions = 0
let deletions = 0
for (const change of diffLines(oldContent, newContent)) {
for (const change of diffLines(oldContent, next.text)) {
if (change.added) additions += change.count || 0
if (change.removed) deletions += change.count || 0
}
@@ -84,11 +87,12 @@ export const ApplyPatchTool = Tool.define(
fileChanges.push({
filePath,
oldContent,
newContent,
newContent: next.text,
type: "add",
diff,
additions,
deletions,
bom: next.bom,
})
totalDiff += diff + "\n"
@@ -104,13 +108,16 @@ export const ApplyPatchTool = Tool.define(
)
}
const oldContent = yield* afs.readFileString(filePath)
const source = yield* Bom.readFile(afs, filePath)
const oldContent = source.text
let newContent = oldContent
let bom = source.bom
// Apply the update chunks to get new content
try {
const fileUpdate = Patch.deriveNewContentsFromChunks(filePath, hunk.chunks)
newContent = fileUpdate.content
bom = fileUpdate.bom
} catch (error) {
return yield* Effect.fail(new Error(`apply_patch verification failed: ${error}`))
}
@@ -136,6 +143,7 @@ export const ApplyPatchTool = Tool.define(
diff,
additions,
deletions,
bom,
})
totalDiff += diff + "\n"
@@ -143,17 +151,16 @@ export const ApplyPatchTool = Tool.define(
}
case "delete": {
const contentToDelete = yield* afs
.readFileString(filePath)
.pipe(
Effect.catch((error) =>
Effect.fail(
new Error(
`apply_patch verification failed: ${error instanceof Error ? error.message : String(error)}`,
),
const source = yield* Bom.readFile(afs, filePath).pipe(
Effect.catch((error) =>
Effect.fail(
new Error(
`apply_patch verification failed: ${error instanceof Error ? error.message : String(error)}`,
),
),
)
),
)
const contentToDelete = source.text
const deleteDiff = trimDiff(createTwoFilesPatch(filePath, filePath, contentToDelete, ""))
const deletions = contentToDelete.split("\n").length
@@ -166,6 +173,7 @@ export const ApplyPatchTool = Tool.define(
diff: deleteDiff,
additions: 0,
deletions,
bom: source.bom,
})
totalDiff += deleteDiff + "\n"
@@ -207,12 +215,12 @@ export const ApplyPatchTool = Tool.define(
case "add":
// Create parent directories (recursive: true is safe on existing/root dirs)
yield* afs.writeWithDirs(change.filePath, change.newContent)
yield* afs.writeWithDirs(change.filePath, Bom.join(change.newContent, change.bom))
updates.push({ file: change.filePath, event: "add" })
break
case "update":
yield* afs.writeWithDirs(change.filePath, change.newContent)
yield* afs.writeWithDirs(change.filePath, Bom.join(change.newContent, change.bom))
updates.push({ file: change.filePath, event: "change" })
break
@@ -220,7 +228,7 @@ export const ApplyPatchTool = Tool.define(
if (change.movePath) {
// Create parent directories (recursive: true is safe on existing/root dirs)
yield* afs.writeWithDirs(change.movePath!, change.newContent)
yield* afs.writeWithDirs(change.movePath!, Bom.join(change.newContent, change.bom))
yield* afs.remove(change.filePath)
updates.push({ file: change.filePath, event: "unlink" })
updates.push({ file: change.movePath, event: "add" })
@@ -234,7 +242,9 @@ export const ApplyPatchTool = Tool.define(
}
if (edited) {
yield* format.file(edited)
if (yield* format.file(edited)) {
yield* Bom.syncFile(afs, edited, change.bom)
}
yield* bus.publish(File.Event.Edited, { file: edited })
}
}
@@ -248,7 +258,7 @@ export const ApplyPatchTool = Tool.define(
for (const change of fileChanges) {
if (change.type === "delete") continue
const target = change.movePath ?? change.filePath
yield* lsp.touchFile(target, true)
yield* lsp.touchFile(target, "document")
}
const diagnostics = yield* lsp.diagnostics()
+29 -16
View File
@@ -18,6 +18,7 @@ import { Instance } from "../project/instance"
import { Snapshot } from "@/snapshot"
import { assertExternalDirectoryEffect } from "./external-directory"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import * as Bom from "@/util/bom"
function normalizeLineEndings(text: string): string {
return text.replaceAll("\r\n", "\n")
@@ -84,7 +85,11 @@ export const EditTool = Tool.define(
Effect.gen(function* () {
if (params.oldString === "") {
const existed = yield* afs.existsSafe(filePath)
contentNew = params.newString
const source = existed ? yield* Bom.readFile(afs, filePath) : { bom: false, text: "" }
const next = Bom.split(params.newString)
const desiredBom = source.bom || next.bom
contentOld = source.text
contentNew = next.text
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
yield* ctx.ask({
permission: "edit",
@@ -95,8 +100,10 @@ export const EditTool = Tool.define(
diff,
},
})
yield* afs.writeWithDirs(filePath, params.newString)
yield* format.file(filePath)
yield* afs.writeWithDirs(filePath, Bom.join(contentNew, desiredBom))
if (yield* format.file(filePath)) {
contentNew = yield* Bom.syncFile(afs, filePath, desiredBom)
}
yield* bus.publish(File.Event.Edited, { file: filePath })
yield* bus.publish(FileWatcher.Event.Updated, {
file: filePath,
@@ -108,13 +115,16 @@ export const EditTool = Tool.define(
const info = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!info) throw new Error(`File ${filePath} not found`)
if (info.type === "Directory") throw new Error(`Path is a directory, not a file: ${filePath}`)
contentOld = yield* afs.readFileString(filePath)
const source = yield* Bom.readFile(afs, filePath)
contentOld = source.text
const ending = detectLineEnding(contentOld)
const old = convertToLineEnding(normalizeLineEndings(params.oldString), ending)
const next = convertToLineEnding(normalizeLineEndings(params.newString), ending)
const replacement = convertToLineEnding(normalizeLineEndings(params.newString), ending)
contentNew = replace(contentOld, old, next, params.replaceAll)
const next = Bom.split(replace(contentOld, old, replacement, params.replaceAll))
const desiredBom = source.bom || next.bom
contentNew = next.text
diff = trimDiff(
createTwoFilesPatch(
@@ -134,14 +144,15 @@ export const EditTool = Tool.define(
},
})
yield* afs.writeWithDirs(filePath, contentNew)
yield* format.file(filePath)
yield* afs.writeWithDirs(filePath, Bom.join(contentNew, desiredBom))
if (yield* format.file(filePath)) {
contentNew = yield* Bom.syncFile(afs, filePath, desiredBom)
}
yield* bus.publish(File.Event.Edited, { file: filePath })
yield* bus.publish(FileWatcher.Event.Updated, {
file: filePath,
event: "change",
})
contentNew = yield* afs.readFileString(filePath)
diff = trimDiff(
createTwoFilesPatch(
filePath,
@@ -153,15 +164,17 @@ export const EditTool = Tool.define(
}).pipe(Effect.orDie),
)
let additions = 0
let deletions = 0
for (const change of diffLines(contentOld, contentNew)) {
if (change.added) additions += change.count || 0
if (change.removed) deletions += change.count || 0
}
const filediff: Snapshot.FileDiff = {
file: filePath,
patch: diff,
additions: 0,
deletions: 0,
}
for (const change of diffLines(contentOld, contentNew)) {
if (change.added) filediff.additions += change.count || 0
if (change.removed) filediff.deletions += change.count || 0
additions,
deletions,
}
yield* ctx.metadata({
@@ -173,7 +186,7 @@ export const EditTool = Tool.define(
})
let output = "Edit applied successfully."
yield* lsp.touchFile(filePath, true)
yield* lsp.touchFile(filePath, "document")
const diagnostics = yield* lsp.diagnostics()
const normalizedFilePath = AppFileSystem.normalizePath(filePath)
const block = LSP.Diagnostic.report(filePath, diagnostics[normalizedFilePath] ?? [])
+1 -1
View File
@@ -55,7 +55,7 @@ export const LspTool = Tool.define(
const available = yield* lsp.hasClients(file)
if (!available) throw new Error("No LSP server available for this file type.")
yield* lsp.touchFile(file, true)
yield* lsp.touchFile(file, "document")
const result: unknown[] = yield* (() => {
switch (args.operation) {
+1 -1
View File
@@ -75,7 +75,7 @@ export const ReadTool = Tool.define(
})
const warm = Effect.fn("ReadTool.warm")(function* (filepath: string) {
yield* lsp.touchFile(filepath, false).pipe(Effect.ignore, Effect.forkIn(scope))
yield* lsp.touchFile(filepath).pipe(Effect.ignore, Effect.forkIn(scope))
})
const readSample = Effect.fn("ReadTool.readSample")(function* (
+2 -3
View File
@@ -1,8 +1,7 @@
import { Schema } from "effect"
import z from "zod"
import { Identifier } from "@/id/id"
import { ZodOverride } from "@/util/effect-zod"
import { zod, ZodOverride } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const toolIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("tool") }).pipe(Schema.brand("ToolID"))
@@ -12,6 +11,6 @@ export type ToolID = typeof toolIdSchema.Type
export const ToolID = toolIdSchema.pipe(
withStatics((schema: typeof toolIdSchema) => ({
ascending: (id?: string) => schema.make(Identifier.ascending("tool", id)),
zod: Identifier.schema("tool").pipe(z.custom<ToolID>()),
zod: zod(schema),
})),
)

Some files were not shown because too many files have changed in this diff Show More