Compare commits

..
72 Commits
Author SHA1 Message Date
opencode 447a4ca8c3 release: v1.0.81
Update Nix Hashes / update (push) Has been cancelled
2025-11-20 17:16:31 +00:00
Aiden Cline 40ac2549ff fix: aur build 2025-11-20 11:04:09 -06:00
cc-bjojacGitHubBjörn Jacobsopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>rekram1-node
a9c56b813a Make the MAX_OUTPUT_LENGTH in bash.ts overridable by OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH (#4497)
Co-authored-by: Björn Jacobs <bjoern.jacobs.ext@bundesanzeiger.de>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-20 10:45:13 -06:00
b1b73c9deb fix: Autocomplete with existing space after trigger (#4121)
Co-authored-by: GitHub Action <action@github.com>
2025-11-20 10:37:45 -06:00
Aiden Cline 774377330b fix: lockfile 2025-11-20 10:16:34 -06:00
processtraderandGitHub e7a157ef8f fix: replace ruby-lsp with rubocop for better LSP performance (#4543) 2025-11-20 09:54:47 -06:00
Frank 3989b9fc7f wip: zen 2025-11-20 10:31:35 -05:00
GitHub Action 8bfcdf4831 chore: format code 2025-11-20 14:55:06 +00:00
Frank 3632ba3785 zen: show token breakdown 2025-11-20 09:54:22 -05:00
b7b3824d76 nix: update flake.lock as part of the github workflow (#4535)
Co-authored-by: Github Action <action@github.com>
2025-11-20 08:43:13 -06:00
b12efb2023 fix: do not print ai sdk warnings (#4541)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-20 08:42:24 -06:00
Roberto GongoraandGitHub bd91cf220c docs: typo in PHP Intelephense documentation (#4545) 2025-11-20 08:41:31 -06:00
Asmae_ELAZRAKandGitHub 9eb6731c21 docs: add cortecs docs (#4547) 2025-11-20 08:34:04 -06:00
GitHub Action 11373755d9 ignore: update download stats 2025-11-20 2025-11-20 12:04:55 +00:00
Aiden Cline 00b5e9f6ca chore: rm unused code 2025-11-20 00:10:51 -06:00
Aiden Cline 6b3f424e4d tweak: let user name export file 2025-11-19 23:58:40 -06:00
Aiden Cline e7dfeec9c4 fix: make toast wrap 2025-11-19 22:50:27 -06:00
Aiden Cline 97893bd7e6 tweak: make /export go to cwd 2025-11-19 22:45:40 -06:00
Frank bfefdb3752 zen: add gemini icon 2025-11-19 21:14:31 -05:00
Frank 12b79c581e zen: update gemini provider name 2025-11-19 20:58:22 -05:00
Frank ac9b4c7ebf docs: add gemini 3 pricing 2025-11-19 20:50:43 -05:00
GitHub Actionandopencode 208af232ff chore: format code 2025-11-20 01:27:50 +00:00
opencode 600c6b4973 release: v1.0.80
Update Nix Hashes / update (push) Has been cancelled
2025-11-20 01:27:49 +00:00
Dax Raad 61007a9b94 refactor: switch to Switch/Match pattern for assistant message status rendering 2025-11-19 20:18:15 -05:00
opencode 52fe1a5ac5 release: v1.0.79
Update Nix Hashes / update (push) Has been cancelled
2025-11-20 01:11:20 +00:00
468927e06a chore: bump ai-sdk to v5.0.97 (#4518)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-19 18:44:33 -06:00
Aiden ClineandGitHub 61562dd9f0 make aur build check if u are glibc system or a musl system (#4519) 2025-11-19 18:36:33 -06:00
Github Action c86dd91310 Update Nix hashes 2025-11-19 23:30:37 +00:00
Sebastian Herrlinger 9c85a37811 bump opentui version to v0.1.47
- fixing cursor issues with some graphemes in textarea
- proper suspend/resume
2025-11-20 00:28:25 +01:00
Aiden Cline 51bba6e634 tweak: default to disabling fetch timeout in provider options 2025-11-19 16:20:29 -06:00
e1089bc5de Adding LSP: PHP Intelephense (#4504)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-19 16:01:18 -06:00
Aiden Cline 618c654aa0 ignore: todo fix test case 2025-11-19 15:18:21 -06:00
4703e859bd Add YAML language server support (#4508)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-19 14:47:04 -06:00
Aiden Cline a1dc4ebbe4 ignore: flaky test be a lil less flaky plz 2025-11-19 14:46:06 -06:00
Aiden Cline e4e6096510 ignore: fix hanging test 2025-11-19 14:38:12 -06:00
Aiden Cline c472734933 tweak: make getUsage function handle missing usage data 2025-11-19 14:29:19 -06:00
Aiden Cline 9d068c20bb fix: openrouter ai sdk package support 2025-11-19 14:22:51 -06:00
Aiden Cline 48e4f2f45d tweak: add bun install retries 2025-11-19 13:04:20 -06:00
Aiden Cline bbf4574476 fix: make external_directory permission wildcarding more sane 2025-11-19 12:55:02 -06:00
Adam 8bad513140 Revert "feat(cli): better install script output"
This reverts commit 24bb293136.
2025-11-19 12:44:35 -06:00
Aiden Cline 1ff5d888c2 fix: make bash tool use external_directory perm 2025-11-19 12:31:34 -06:00
Dax Raad 5d25758400 use bash description as task title 2025-11-19 13:23:29 -05:00
DaxandGitHub 16fdc90976 fix: resolve issue 4475 (#4505) 2025-11-19 13:10:09 -05:00
Aiden Cline 793542230f tweak: bash description 2025-11-19 11:31:12 -06:00
9de1242d9b fix: show reasoning summaries for gemini models (#4491)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-19 11:25:18 -06:00
Aiden Cline b3afa84058 Revert "Added subagents to agents modal, non-selectable (#4460)"
This reverts commit 90044196bf.
2025-11-19 11:00:38 -06:00
Aiden Cline 024a10bbb5 ci: auto label nix 2025-11-19 10:51:11 -06:00
Adam bef9ac96e2 fix(web): stats 2025-11-19 10:05:39 -06:00
Adam 24bb293136 feat(cli): better install script output 2025-11-19 09:30:41 -06:00
Adam 45180104fe fix(desktop): message animation 2025-11-19 06:04:20 -06:00
Adam edd86e3fb7 fix(desktop): text part styling 2025-11-19 06:04:20 -06:00
Adam 4a72d57534 fix(desktop): pre styling 2025-11-19 06:04:19 -06:00
Aiden Cline 0068cb305f tweak: toast 2025-11-19 00:51:07 -06:00
90044196bf Added subagents to agents modal, non-selectable (#4460)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-19 00:40:47 -06:00
963a926db2 allow task tool to have resume capabilities (#4204)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-19 00:17:26 -06:00
Frank 0d3d48bb59 zen: fix cost in graph 2025-11-18 23:43:26 -05:00
GitHub Action 66eaba4bdc chore: format code 2025-11-19 01:02:57 +00:00
Dax Raad 21b6e5404e feat: add @opencode-ai/util package with utility functions 2025-11-18 20:02:10 -05:00
GitHub Action a0fe59ab75 chore: format code 2025-11-18 23:49:29 +00:00
Aiden Cline 81ebf56cf1 feat: add top level lsp: false and formatter: false to allow disabling all formatters or lsps at once 2025-11-18 17:48:40 -06:00
opencode 429708e3d5 release: v1.0.78
Update Nix Hashes / update (push) Has been cancelled
2025-11-18 23:36:35 +00:00
d50f825c6d fix: pass model info to ReadTool to enable image support check (#4473)
Co-authored-by: GitHub Action <action@github.com>
2025-11-18 17:20:03 -06:00
47bfae52c0 fix: permission checks for external_directory and doom_loop (#4433)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: AerionDyseti <AerionDyseti@users.noreply.github.com>
2025-11-18 17:18:23 -06:00
Frank 52cf9e3423 wip: zen 2025-11-18 17:42:48 -05:00
GitHub Action a9b6debfa2 chore: format code 2025-11-18 22:24:29 +00:00
Eric JudenandGitHub d6bf475749 docs: Improving Plugin Documentation - Adding Events (#4438) 2025-11-18 16:23:46 -06:00
opencode f22580e943 release: v1.0.77
Update Nix Hashes / update (push) Has been cancelled
2025-11-18 22:17:39 +00:00
Dax Raad 6d98db57c7 better gemini retry errors 2025-11-18 17:11:29 -05:00
OpeOginniandGitHub 59f127a250 fix: allow for theme references (#4450) 2025-11-18 14:26:42 -06:00
Adam 3068e7dcf7 fix(desktop): animating too much 2025-11-18 14:24:26 -06:00
GitHub Action f83d62191a chore: format code 2025-11-18 20:23:29 +00:00
LongloneGitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>rekram1-node
3b72857124 fix: update reasoningEffort logic for gpt-5.1 models in SessionPrompt-ensureTitle (#4456)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-18 14:22:44 -06:00
72 changed files with 1124 additions and 358 deletions
+16 -8
View File
@@ -28,14 +28,14 @@ jobs:
const versionPattern = /[v]?1\.0\./i;
const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
// Check for "nix" keyword
const nixPattern = /\bnix\b/i;
const isNixRelated = nixPattern.test(title) || nixPattern.test(description);
const labels = [];
if (isWebRelated) {
// Add web label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['web']
});
labels.push('web');
// Assign to adamdotdevin
await github.rest.issues.addAssignees({
@@ -46,10 +46,18 @@ jobs:
});
} else if (isVersionRelated) {
// Only add opentui if NOT web-related
labels.push('opentui');
}
if (isNixRelated) {
labels.push('nix');
}
if (labels.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['opentui']
labels: labels
});
}
+7 -2
View File
@@ -37,6 +37,11 @@ jobs:
git config --global user.email "action@github.com"
git config --global user.name "Github Action"
- name: Update flake.lock
run: |
set -euo pipefail
nix flake update
- name: Update node_modules hash
run: |
set -euo pipefail
@@ -62,7 +67,7 @@ jobs:
echo "" >> "$GITHUB_STEP_SUMMARY"
}
FILES=(flake.nix nix/node-modules.nix nix/hashes.json)
FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json)
STATUS="$(git status --short -- "${FILES[@]}" || true)"
if [ -z "$STATUS" ]; then
summarize "no changes"
@@ -71,7 +76,7 @@ jobs:
fi
git add "${FILES[@]}"
git commit -m "Update Nix hashes"
git commit -m "Update Nix flake.lock and hashes"
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
git push origin HEAD:"$BRANCH"
+2
View File
@@ -144,3 +144,5 @@
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
+45 -26
View File
@@ -41,7 +41,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.76",
"version": "1.0.81",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -68,7 +68,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.76",
"version": "1.0.81",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -92,7 +92,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.76",
"version": "1.0.81",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -116,7 +116,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.76",
"version": "1.0.81",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -156,7 +156,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.76",
"version": "1.0.81",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -172,7 +172,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.76",
"version": "1.0.81",
"bin": {
"opencode": "./bin/opencode",
},
@@ -180,6 +180,7 @@
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.5.1",
"@ai-sdk/mcp": "0.0.8",
"@clack/prompts": "1.0.0-alpha.1",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
@@ -190,8 +191,8 @@
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opentui/core": "0.1.46",
"@opentui/solid": "0.1.46",
"@opentui/core": "0.1.47",
"@opentui/solid": "0.1.47",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -250,7 +251,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.76",
"version": "1.0.81",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -270,7 +271,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.76",
"version": "1.0.81",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@@ -281,7 +282,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.76",
"version": "1.0.81",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -294,7 +295,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.76",
"version": "1.0.81",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -322,9 +323,19 @@
"vite-plugin-solid": "catalog:",
},
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.81",
"dependencies": {
"zod": "catalog:",
},
"devDependencies": {
"typescript": "catalog:",
},
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.76",
"version": "1.0.81",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -378,7 +389,7 @@
"@types/bun": "1.3.0",
"@types/node": "22.13.9",
"@typescript/native-preview": "7.0.0-dev.20251014.1",
"ai": "5.0.8",
"ai": "5.0.97",
"diff": "8.0.2",
"fuzzysort": "3.1.0",
"hono": "4.7.10",
@@ -413,12 +424,14 @@
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.4", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-1roLdgMbFU3Nr4MC97/te7w6OqxsWBkDUkpbCcvxF3jz/ku91WVaJldn/PKU8feMKNyI5W9wnqhbjb1BqbExOQ=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.12", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W+cB1sOWvPcz9qiIsNtD+HxUrBUva2vWv2K1EFukuImX+HA0uZx3EyyOjhYQ9gtf/teqEG80M6OvJ7xx/VLV2A=="],
"@ai-sdk/google": ["@ai-sdk/google@2.0.11", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.7" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-dnVIgSz1DZD/0gVau6ifYN3HZFN15HZwC9VjevTFfvrfSfbEvpXj5x/k/zk/0XuQrlQ5g8JiwJtxc9bx24x2xw=="],
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.16", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.9", "@ai-sdk/google": "2.0.11", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.7", "google-auth-library": "^9.15.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-tStlnOCRGRqKKJSCOtXhijX4r9kYVK2v+Vs7miJnfvr3sZfO8nRS0xnNhfgu17xuNi5LMMufeCYURTz4lKxzUQ=="],
"@ai-sdk/mcp": ["@ai-sdk/mcp@0.0.8", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "pkce-challenge": "^5.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9y9GuGcZ9/+pMIHfpOCJgZVp+AZMv6TkjX2NVT17SQZvTF2N8LXuCXyoUPyi1PxIxzxl0n463LxxaB2O6olC+Q=="],
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="],
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-luHVcU+yKzwv3ekKgbP3v+elUVxb2Rt+8c6w9qi7g2NYG2/pEL21oIrnaEnc6UtTZLLZX9EFBcpq2N1FQKDIMw=="],
@@ -965,25 +978,27 @@
"@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"],
"@opencode-ai/util": ["@opencode-ai/util@workspace:packages/util"],
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.1.46", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.46", "@opentui/core-darwin-x64": "0.1.46", "@opentui/core-linux-arm64": "0.1.46", "@opentui/core-linux-x64": "0.1.46", "@opentui/core-win32-arm64": "0.1.46", "@opentui/core-win32-x64": "0.1.46", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-9682jrr65vYP0hPHfrZRK3xymlCSLVBMrRKNtclFasDi6bRvACUrtziFOIIyMIvPHRJCFWPbtz0MppARmN4zvQ=="],
"@opentui/core": ["@opentui/core@0.1.47", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.47", "@opentui/core-darwin-x64": "0.1.47", "@opentui/core-linux-arm64": "0.1.47", "@opentui/core-linux-x64": "0.1.47", "@opentui/core-win32-arm64": "0.1.47", "@opentui/core-win32-x64": "0.1.47", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-gKcYX9EJ/e5VLEwBH2kalDr5xoI9MEanzQV7uV3Sb2Z9+ndwEUShKKna3odN8g4E20c4sX2VpwmB9hhl3Tsd9w=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.46", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sp/uFS8J/1yVfhgkTJ43OZfy64hv1/9sdT+oC5yb8XTNPI3QGtg6ixjr3nRoD/Lkxuj2i5SJ30RZufqH6rkCpA=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.47", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0/u4VkJJPvW24cZzMaKf6Dm+VzeO1a94l6NV3AQ1Wb+pPTEyOmNWkRvj03ZrRLMCyQduaFVtlnor8DVCk6OHuQ=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.46", "", { "os": "darwin", "cpu": "x64" }, "sha512-JtxEruRyLQRK8ByPmBm1nYYSvnX6mXNC+mngvd5RDiCzLzkM6qVBQBd/m3Hxp2/s6MO5Z2+iVBzZ8XFH5T4IZw=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.47", "", { "os": "darwin", "cpu": "x64" }, "sha512-y1+c/e+IaZAj5N02GnD+oaubbb5JiW5eKgF0h58kw73iXDMfynuoGOpREz58i1rUFYOMYJGdrSjEHtXk2pD2XA=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.46", "", { "os": "linux", "cpu": "arm64" }, "sha512-pN8nR4CwBlkZ5uh5KvoytiXXav2GhkP9cB2d3gPe49c7MBz2XrjGexgb47xjaq0hAVbytv9XUifqdPTcFQdPaQ=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.47", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZESHmqILtfb6FFEfi40JGKl8z0+LhOSoHgfOK1PPyuyRT9Mk8uXeQgPMF5W6Ac0pp4w+uWVC4TrFjijCCSiaUQ=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.46", "", { "os": "linux", "cpu": "x64" }, "sha512-oH4/FEYZYce9qMQVqGl4+Btw+Mfsf6ybpWIIJUJjXMWWZlAgsTMAWM8m195Oe6WstfFLF+nRH7NUcm/YOsCHnw=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.47", "", { "os": "linux", "cpu": "x64" }, "sha512-qfvy1qshgnZMcAHQ3MS093IBjxM2pPx+kEnW7icsyud60zoJgoUugdN2kjgJiIJiYX3f3PgE68J6CVW2MCtYfQ=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.46", "", { "os": "win32", "cpu": "arm64" }, "sha512-C/rTBJ9bzBcZJRCLIxi9Ka/DANe2SaHtryotseWPk9RDydw7LTHGoi3VtRW0RFijQGqmvFg+31MeNhvY1YZ65Q=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.47", "", { "os": "win32", "cpu": "arm64" }, "sha512-f6OoPnaz303H6fudi8blS+iEcJtlFlcqdBoWnWnJQfN9rLmajW3Yf7RfpNOoLUlDcwxQLyTL/5EHwbcG8D4r7A=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.46", "", { "os": "win32", "cpu": "x64" }, "sha512-d2DXSlA93LbSriX+pDDZ5sMwkcW1+eVoeykxeW4UParSb4/3ceBCD4aSARaZ6yoq0rR1IWOdgKdiihZH4mwdJQ=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.47", "", { "os": "win32", "cpu": "x64" }, "sha512-lQnJg7FucyyTbN/ybTj5FZ7S8OAfT5KxXDR5l9Sla7R5MIDY6nBXYM3GWeF81jzDd4K4Z/0hxNFtWSopEXRFYg=="],
"@opentui/solid": ["@opentui/solid@0.1.46", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.46", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-t+LDXS+FBT+fjQHOxuL44Bx1jGuXuLouyB25BZuypQKKT8sOhi8rJ5+Q4UwH5lrI4OoRBXmzrgsWj7DD58sHDw=="],
"@opentui/solid": ["@opentui/solid@0.1.47", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.47", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-azN2sf8X/6HiLkz8ip2lcY532ApNEkl+BHd+wml/HdwdgLE7nthgA6x8Pgvi7f4qkRmpeYATU+danIzB6K6B8A=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -1495,6 +1510,8 @@
"@vercel/nft": ["@vercel/nft@0.30.3", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^10.4.5", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-UEq+eF0ocEf9WQCV1gktxKhha36KDs7jln5qii6UpPf5clMqDc0p3E7d9l2Smx0i9Pm1qpq4S4lLfNl97bbv6w=="],
"@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="],
"@vinxi/listhen": ["@vinxi/listhen@1.5.6", "", { "dependencies": { "@parcel/watcher": "^2.3.0", "@parcel/watcher-wasm": "2.3.0", "citty": "^0.1.5", "clipboardy": "^4.0.0", "consola": "^3.2.3", "defu": "^6.1.4", "get-port-please": "^3.1.2", "h3": "^1.10.0", "http-shutdown": "^1.2.2", "jiti": "^1.21.0", "mlly": "^1.5.0", "node-forge": "^1.3.1", "pathe": "^1.1.2", "std-env": "^3.7.0", "ufo": "^1.3.2", "untun": "^0.1.3", "uqr": "^0.1.2" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-WSN1z931BtasZJlgPp704zJFnQFRg7yzSjkm3MzAWQYe4uXFXlFr1hc5Ac2zae5/HDOz5x1/zDM5Cb54vTCnWw=="],
"@vinxi/plugin-directives": ["@vinxi/plugin-directives@0.5.1", "", { "dependencies": { "@babel/parser": "^7.23.5", "acorn": "^8.10.0", "acorn-jsx": "^5.3.2", "acorn-loose": "^8.3.0", "acorn-typescript": "^1.4.3", "astring": "^1.8.6", "magicast": "^0.2.10", "recast": "^0.23.4", "tslib": "^2.6.2" }, "peerDependencies": { "vinxi": "^0.5.5" } }, "sha512-pH/KIVBvBt7z7cXrUH/9uaqcdxjegFC7+zvkZkdOyWzs+kQD5KPf3cl8kC+5ayzXHT+OMlhGhyitytqN3cGmHg=="],
@@ -1529,7 +1546,7 @@
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ai": ["ai@5.0.8", "", { "dependencies": { "@ai-sdk/gateway": "1.0.4", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-qbnhj046UvG30V1S5WhjBn+RBGEAmi8PSZWqMhRsE3EPxvO5BcePXTZFA23e9MYyWS9zr4Vm8Mv3wQXwLmtIBw=="],
"ai": ["ai@5.0.97", "", { "dependencies": { "@ai-sdk/gateway": "2.0.12", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8zBx0b/owis4eJI2tAlV8a1Rv0BANmLxontcAelkLNwEHhgfgXeKpDkhNB6OgV+BJSwboIUDkgd9312DdJnCOQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
@@ -3603,7 +3620,7 @@
"@ai-sdk/amazon-bedrock/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g=="],
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
"@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA=="],
@@ -3611,6 +3628,8 @@
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA=="],
"@ai-sdk/mcp/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
"@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
@@ -3819,7 +3838,7 @@
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g=="],
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1762156382,
"narHash": "sha256-Yg7Ag7ov5+36jEFC1DaZh/12SEXo6OO3/8rqADRxiqs=",
"lastModified": 1763464769,
"narHash": "sha256-AJHrsT7VoeQzErpBRlLJM1SODcaayp0joAoEA35yiwM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7241bcbb4f099a66aafca120d37c65e8dda32717",
"rev": "6f374686605df381de8541c072038472a5ea2e2d",
"type": "github"
},
"original": {
+1 -1
View File
@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-lAU5G92UJ998pKOd0EAt9gUa/HUXXvu6xAUdH2STmDo="
"nodeModules": "sha256-bPiUpHGtgwVxHQHXBprpc6fFeJqW6/x7dwtQZBq29oU="
}
+1 -1
View File
@@ -32,7 +32,7 @@
"@solidjs/meta": "0.29.4",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"ai": "5.0.8",
"ai": "5.0.97",
"hono": "4.7.10",
"fuzzysort": "3.1.0",
"luxon": "3.6.1",
+1 -1
View File
@@ -7,7 +7,7 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "1.0.76"
"version": "1.0.81"
},
"dependencies": {
"@ibm/plex": "6.4.1",
@@ -202,6 +202,14 @@ export function IconZai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconGoogle(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 50 50" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M49.04,24.001l-1.082-0.043h-0.001C36.134,23.492,26.508,13.866,26.042,2.043L25.999,0.96C25.978,0.424,25.537,0,25,0 s-0.978,0.424-0.999,0.96l-0.043,1.083C23.492,13.866,13.866,23.492,2.042,23.958L0.96,24.001C0.424,24.022,0,24.463,0,25 c0,0.537,0.424,0.978,0.961,0.999l1.082,0.042c11.823,0.467,21.449,10.093,21.915,21.916l0.043,1.083C24.022,49.576,24.463,50,25,50 s0.978-0.424,0.999-0.96l0.043-1.083c0.466-11.823,10.092-21.449,21.915-21.916l1.082-0.042C49.576,25.978,50,25.537,50,25 C50,24.463,49.576,24.022,49.04,24.001z"></path>
</svg>
)
}
export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="none">
@@ -228,3 +236,14 @@ export function IconChevronRight(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
</svg>
)
}
export function IconBreakdown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M2 12L2 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M6 12L6 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M10 12L10 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M14 12L14 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
)
}
+2 -2
View File
@@ -22,8 +22,8 @@ export const config = {
// Static stats (used on landing page)
stats: {
contributors: "250",
commits: "3,500",
contributors: "300",
commits: "4,000",
monthlyUsers: "300,000",
},
} as const
@@ -1,4 +1,4 @@
import { and, Database, eq, gte, inArray, isNull, lte, or, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { and, Database, eq, gte, inArray, isNull, lte, or, sql, sum } from "@opencode-ai/console-core/drizzle/index.js"
import { UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
@@ -35,7 +35,7 @@ async function getCosts(workspaceID: string, year: number, month: number) {
.select({
date: sql<string>`DATE(${UsageTable.timeCreated})`,
model: UsageTable.model,
totalCost: sql<number>`SUM(${UsageTable.cost})`,
totalCost: sum(UsageTable.cost),
keyId: UsageTable.keyID,
})
.from(UsageTable)
@@ -46,7 +46,13 @@ async function getCosts(workspaceID: string, year: number, month: number) {
lte(UsageTable.timeCreated, endDate),
),
)
.groupBy(sql`DATE(${UsageTable.timeCreated})`, UsageTable.model, UsageTable.keyID),
.groupBy(sql`DATE(${UsageTable.timeCreated})`, UsageTable.model, UsageTable.keyID)
.then((x) =>
x.map((r) => ({
...r,
totalCost: r.totalCost ? parseInt(r.totalCost) : 0,
})),
),
)
// Get unique key IDs from usage
@@ -204,18 +210,16 @@ export function GraphSection() {
const dates = getDates()
if (!data?.usage?.length) return null
const filteredUsageResults = store.key ? data.usage.filter((row) => row.keyId === store.key) : data.usage
const dailyData = new Map<string, Map<string, number>>()
for (const dateKey of dates) dailyData.set(dateKey, new Map())
for (const row of filteredUsageResults) {
const dayMap = dailyData.get(row.date)
if (dayMap) {
const existing = dayMap.get(row.model) || 0
dayMap.set(row.model, existing + row.totalCost)
}
}
data.usage
.filter((row) => (store.key ? row.keyId === store.key : true))
.forEach((row) => {
const dayMap = dailyData.get(row.date)
if (!dayMap) return
dayMap.set(row.model, (dayMap.get(row.model) ?? 0) + row.totalCost)
})
const filteredModels = store.model === null ? getModels() : [store.model]
@@ -223,7 +227,7 @@ export function GraphSection() {
const color = getModelColor(model)
return {
label: model,
data: dates.map((date) => (dailyData.get(date)?.get(model) || 0) / 100000000),
data: dates.map((date) => (dailyData.get(date)?.get(model) || 0) / 100_000_000),
backgroundColor: color,
hoverBackgroundColor: color,
borderWidth: 0,
@@ -5,11 +5,21 @@ import { withActor } from "~/context/auth.withActor"
import { ZenData } from "@opencode-ai/console-core/model.js"
import styles from "./model-section.module.css"
import { querySessionInfo } from "../common"
import { IconAlibaba, IconAnthropic, IconMoonshotAI, IconOpenAI, IconStealth, IconXai, IconZai } from "~/component/icon"
import {
IconAlibaba,
IconAnthropic,
IconGoogle,
IconMoonshotAI,
IconOpenAI,
IconStealth,
IconXai,
IconZai,
} from "~/component/icon"
const getModelLab = (modelId: string) => {
if (modelId.startsWith("claude")) return "Anthropic"
if (modelId.startsWith("gpt")) return "OpenAI"
if (modelId.startsWith("gemini")) return "Google"
if (modelId.startsWith("kimi")) return "Moonshot AI"
if (modelId.startsWith("glm")) return "Z.ai"
if (modelId.startsWith("qwen")) return "Alibaba"
@@ -24,7 +34,17 @@ const getModelsInfo = query(async (workspaceID: string) => {
all: Object.entries(ZenData.list().models)
.filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
.filter(([id, _model]) => !id.startsWith("alpha-"))
.sort(([_idA, modelA], [_idB, modelB]) => modelA.name.localeCompare(modelB.name))
.sort(([idA, modelA], [idB, modelB]) => {
const priority = ["big-pickle", "grok", "claude", "gpt", "gemini"]
const getPriority = (id: string) => {
const index = priority.findIndex((p) => id.startsWith(p))
return index === -1 ? Infinity : index
}
const pA = getPriority(idA)
const pB = getPriority(idB)
if (pA !== pB) return pA - pB
return modelA.name.localeCompare(modelB.name)
})
.map(([id, model]) => ({ id, name: model.name })),
disabled: await Model.listDisabled(),
}
@@ -96,6 +116,8 @@ export function ModelSection() {
return <IconOpenAI width={16} height={16} />
case "Anthropic":
return <IconAnthropic width={16} height={16} />
case "Google":
return <IconGoogle width={16} height={16} />
case "Moonshot AI":
return <IconMoonshotAI width={16} height={16} />
case "Z.ai":
@@ -56,6 +56,53 @@
color: var(--color-text);
font-weight: 500;
}
[data-slot="tokens-with-breakdown"] {
position: relative;
display: flex;
align-items: center;
gap: var(--space-2);
}
[data-slot="breakdown-button"] {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
background: transparent;
border: none;
color: var(--color-text-muted);
cursor: pointer;
transition: color 0.15s ease;
&:hover {
color: var(--color-text);
}
svg {
width: 16px;
height: 16px;
}
}
[data-slot="breakdown-popup"] {
position: absolute;
left: 0;
top: 100%;
margin-top: var(--space-2);
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
padding: var(--space-2);
z-index: 10;
min-width: 180px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
font-size: var(--font-size-xs);
@media (prefers-color-scheme: dark) {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
}
}
tbody tr:last-child td {
@@ -116,4 +163,24 @@
}
}
}
/* Breakdown popup content */
[data-slot="breakdown-row"] {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-4);
padding: var(--space-1) 0;
}
[data-slot="breakdown-label"] {
color: var(--color-text-muted);
font-size: var(--font-size-xs);
}
[data-slot="breakdown-value"] {
color: var(--color-text);
font-weight: 500;
font-size: var(--font-size-xs);
}
}
@@ -1,9 +1,9 @@
import { Billing } from "@opencode-ai/console-core/billing.js"
import { createAsync, query, useParams } from "@solidjs/router"
import { createMemo, For, Show, createEffect } from "solid-js"
import { createMemo, For, Show, createEffect, createSignal } from "solid-js"
import { formatDateUTC, formatDateForTable } from "../common"
import { withActor } from "~/context/auth.withActor"
import { IconChevronLeft, IconChevronRight } from "~/component/icon"
import { IconChevronLeft, IconChevronRight, IconBreakdown } from "~/component/icon"
import styles from "./usage-section.module.css"
import { createStore } from "solid-js/store"
@@ -22,15 +22,34 @@ export function UsageSection() {
const params = useParams()
const usage = createAsync(() => queryUsageInfo(params.id!, 0))
const [store, setStore] = createStore({ page: 0, usage: [] as Awaited<ReturnType<typeof getUsageInfo>> })
const [openBreakdownId, setOpenBreakdownId] = createSignal<string | null>(null)
createEffect(() => {
setStore({ usage: usage() })
}, [usage])
createEffect(() => {
if (!openBreakdownId()) return
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (!target.closest('[data-slot="tokens-with-breakdown"]')) {
setOpenBreakdownId(null)
}
}
document.addEventListener("click", handleClickOutside)
return () => document.removeEventListener("click", handleClickOutside)
})
const hasResults = createMemo(() => store.usage && store.usage.length > 0)
const canGoPrev = createMemo(() => store.page > 0)
const canGoNext = createMemo(() => store.usage && store.usage.length === PAGE_SIZE)
const calculateTotalInputTokens = (u: Awaited<ReturnType<typeof getUsageInfo>>[0]) => {
return u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWrite5mTokens ?? 0) + (u.cacheWrite1hTokens ?? 0)
}
const goPrev = async () => {
const usage = await getUsageInfo(params.id!, store.page - 1)
setStore({
@@ -73,15 +92,50 @@ export function UsageSection() {
</thead>
<tbody>
<For each={store.usage}>
{(usage) => {
{(usage, index) => {
const date = createMemo(() => new Date(usage.timeCreated))
const totalInputTokens = createMemo(() => calculateTotalInputTokens(usage))
const breakdownId = `breakdown-${index()}`
const isOpen = createMemo(() => openBreakdownId() === breakdownId)
const isClaude = usage.model.toLowerCase().includes("claude")
return (
<tr>
<td data-slot="usage-date" title={formatDateUTC(date())}>
{formatDateForTable(date())}
</td>
<td data-slot="usage-model">{usage.model}</td>
<td data-slot="usage-tokens">{usage.inputTokens}</td>
<td data-slot="usage-tokens">
<div data-slot="tokens-with-breakdown" onClick={(e) => e.stopPropagation()}>
<button
data-slot="breakdown-button"
onClick={(e) => {
e.stopPropagation()
setOpenBreakdownId(isOpen() ? null : breakdownId)
}}
>
<IconBreakdown />
</button>
<span onClick={() => setOpenBreakdownId(null)}>{totalInputTokens()}</span>
<Show when={isOpen()}>
<div data-slot="breakdown-popup" onClick={(e) => e.stopPropagation()}>
<div data-slot="breakdown-row">
<span data-slot="breakdown-label">Input</span>
<span data-slot="breakdown-value">{usage.inputTokens}</span>
</div>
<div data-slot="breakdown-row">
<span data-slot="breakdown-label">Cache Read</span>
<span data-slot="breakdown-value">{usage.cacheReadTokens ?? 0}</span>
</div>
<Show when={isClaude}>
<div data-slot="breakdown-row">
<span data-slot="breakdown-label">Cache Write</span>
<span data-slot="breakdown-value">{usage.cacheWrite5mTokens ?? 0}</span>
</div>
</Show>
</div>
</Show>
</div>
</td>
<td data-slot="usage-tokens">{usage.outputTokens}</td>
<td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
</tr>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.76",
"version": "1.0.81",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.76",
"version": "1.0.81",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
@@ -12,7 +12,8 @@ export default {
if (
url.pathname !== "/zen/v1/chat/completions" &&
url.pathname !== "/zen/v1/messages" &&
url.pathname !== "/zen/v1/responses"
url.pathname !== "/zen/v1/responses" &&
!url.pathname.startsWith("/zen/v1/models/")
)
return
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.76",
"version": "1.0.81",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.76",
"version": "1.0.81",
"description": "",
"type": "module",
"scripts": {
@@ -0,0 +1,20 @@
import { createSignal, onCleanup, onMount } from "solid-js"
import { isServer } from "solid-js/web"
export function createSessionSeen(key: string, delay = 1000) {
// 1. Initialize state based on storage (default to true on server to avoid flash)
const storageKey = `app:seen:${key}`
const [hasSeen] = createSignal(!isServer && sessionStorage.getItem(storageKey) === "true")
onMount(() => {
// 2. If we haven't seen it, mark it as seen for NEXT time
if (!hasSeen()) {
const timer = setTimeout(() => {
sessionStorage.setItem(storageKey, "true")
}, delay)
onCleanup(() => clearTimeout(timer))
}
})
return hasSeen
}
+11 -9
View File
@@ -52,6 +52,7 @@ import { useSession } from "@/context/session"
import { StickyAccordionHeader } from "@/components/sticky-accordion-header"
import { SessionReview } from "@/components/session-review"
import { useLayout } from "@/context/layout"
import { createSessionSeen } from "@/hooks/create-session-seen"
export default function Page() {
const layout = useLayout()
@@ -451,7 +452,9 @@ export default function Page() {
<For each={session.messages.user()}>
{(message) => {
const isActive = createMemo(() => session.messages.active()?.id === message.id)
const [titled, setTitled] = createSignal(!!message.summary?.title)
const titleSeen = createSessionSeen(`message-title-${message.id}`)
const contentSeen = createSessionSeen(`message-content-${message.id}`)
const [titled, setTitled] = createSignal(titleSeen())
const assistantMessages = createMemo(() => {
if (!session.id) return []
return sync.data.message[session.id]?.filter(
@@ -459,8 +462,6 @@ export default function Page() {
) as AssistantMessageType[]
})
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
const initialCompleted = !!message.summary?.body || !!error()
const [completed, setCompleted] = createSignal(initialCompleted)
const [detailsExpanded, setDetailsExpanded] = createSignal(false)
const parts = createMemo(() => sync.data.part[message.id])
const hasToolPart = createMemo(() =>
@@ -471,16 +472,18 @@ export default function Page() {
const working = createMemo(
() => message.id === session.messages.last()?.id && session.working(),
)
const initialCompleted = !(message.id === session.messages.last()?.id && session.working())
const [completed, setCompleted] = createSignal(initialCompleted)
// allowing time for the animations to finish
createEffect(() => {
if (titleSeen()) return
const title = message.summary?.title
setTimeout(() => setTitled(!!title), 10_000)
if (title) setTimeout(() => setTitled(true), 10_000)
})
createEffect(() => {
const summary = message.summary?.body
const complete = !!summary || !!error()
setTimeout(() => setCompleted(complete), 1200)
const completed = !working()
setTimeout(() => setCompleted(completed), 1200)
})
return (
@@ -524,8 +527,7 @@ export default function Page() {
<Markdown
classList={{
"text-14-regular": !!message.summary?.diffs?.length,
"[&>*]:fade-up-text":
!message.summary?.diffs?.length && !initialCompleted,
"[&>*]:fade-up-text": !message.summary?.diffs?.length && !contentSeen(),
}}
text={summary()}
/>
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The AI coding agent built for the terminal"
version = "1.0.76"
version = "1.0.81"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.76/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.81/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.76/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.81/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.76/opencode-linux-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.81/opencode-linux-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.76/opencode-linux-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.81/opencode-linux-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.76/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.81/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.76",
"version": "1.0.81",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+4 -3
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.76",
"version": "1.0.81",
"name": "opencode",
"type": "module",
"private": true,
@@ -44,6 +44,7 @@
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.5.1",
"@ai-sdk/mcp": "0.0.8",
"@clack/prompts": "1.0.0-alpha.1",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
@@ -54,8 +55,8 @@
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opentui/core": "0.1.46",
"@opentui/solid": "0.1.46",
"@opentui/core": "0.1.47",
"@opentui/solid": "0.1.47",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
+9
View File
@@ -167,6 +167,15 @@ export default {
],
},
},
{
filetype: "yaml",
wasm: "https://github.com/tree-sitter-grammars/tree-sitter-yaml/releases/download/v0.7.2/tree-sitter-yaml.wasm",
queries: {
highlights: [
"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/yaml/highlights.scm",
],
},
},
{
filetype: "haskell",
wasm: "https://github.com/tree-sitter/tree-sitter-haskell/releases/download/v0.23.1/tree-sitter-haskell.wasm",
+28 -1
View File
@@ -131,7 +131,34 @@ if (!Script.preview) {
"",
"package() {",
` cd "opencode-\${pkgver}/packages/opencode"`,
' install -Dm755 $(find dist/*/bin/opencode) "${pkgdir}/usr/bin/opencode"',
' mkdir -p "${pkgdir}/usr/bin"',
' target_arch="x64"',
' case "$CARCH" in',
' x86_64) target_arch="x64" ;;',
' aarch64) target_arch="arm64" ;;',
' *) printf "unsupported architecture: %s\\n" "$CARCH" >&2 ; return 1 ;;',
" esac",
' libc=""',
" if command -v ldd >/dev/null 2>&1; then",
" if ldd --version 2>&1 | grep -qi musl; then",
' libc="-musl"',
" fi",
" fi",
' if [ -z "$libc" ] && ls /lib/ld-musl-* >/dev/null 2>&1; then',
' libc="-musl"',
" fi",
' base=""',
' if [ "$target_arch" = "x64" ]; then',
" if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then",
' base="-baseline"',
" fi",
" fi",
' bin="dist/opencode-linux-${target_arch}${base}${libc}/bin/opencode"',
' if [ ! -f "$bin" ]; then',
' printf "unable to find binary for %s%s%s\\n" "$target_arch" "$base" "$libc" >&2',
" return 1",
" fi",
' install -Dm755 "$bin" "${pkgdir}/usr/bin/opencode"',
"}",
"",
].join("\n")
+42 -10
View File
@@ -79,16 +79,48 @@ export namespace BunProc {
version,
})
await BunProc.run(args, {
cwd: Global.Path.cache,
}).catch((e) => {
throw new InstallFailedError(
{ pkg, version },
{
cause: e,
},
)
})
const total = 3
const wait = 500
const runInstall = async (count: number = 1): Promise<void> => {
log.info("bun install attempt", {
pkg,
version,
attempt: count,
total,
})
await BunProc.run(args, {
cwd: Global.Path.cache,
}).catch(async (error) => {
log.warn("bun install failed", {
pkg,
version,
attempt: count,
total,
error,
})
if (count >= total) {
throw new InstallFailedError(
{ pkg, version },
{
cause: error,
},
)
}
const delay = wait * count
log.info("bun install retrying", {
pkg,
version,
next: count + 1,
delay,
})
await Bun.sleep(delay)
return runInstall(count + 1)
})
}
await runInstall()
parsed.dependencies[pkg] = version
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
return mod
@@ -53,15 +53,7 @@ export function Autocomplete(props: {
// Track props.value to make memo reactive to text changes
props.value // <- there surely is a better way to do this, like making .input() reactive
const val = props.input().getTextRange(store.index + 1, props.input().cursorOffset + 1)
// If the filter contains a space, hide the autocomplete
if (val.includes(" ")) {
hide()
return undefined
}
return val
return props.input().getTextRange(store.index + 1, props.input().cursorOffset)
})
function insertPart(text: string, part: PromptInfo["parts"][number]) {
@@ -387,17 +379,19 @@ export function Autocomplete(props: {
get visible() {
return store.visible
},
onInput() {
onInput(value) {
if (store.visible) {
if (props.input().cursorOffset <= store.index) {
if (
// Typed text before the trigger
props.input().cursorOffset <= store.index ||
// There is a space between the trigger and the cursor
props.input().getTextRange(store.index, props.input().cursorOffset).match(/\s/) ||
// "/<command>" is not the sole content
(store.visible === "/" && value.match(/^\S+\s+\S+\s*$/))
) {
hide()
return
}
// Check if a space was typed after the trigger character
const currentText = props.input().getTextRange(store.index + 1, props.input().cursorOffset + 1)
if (currentText.includes(" ")) {
hide()
}
}
},
onKeyDown(e: KeyEvent) {
@@ -9,7 +9,7 @@ import {
fg,
type KeyBinding,
} from "@opentui/core"
import { createEffect, createMemo, Match, Switch, type JSX, onMount, batch } from "solid-js"
import { createEffect, createMemo, Match, Switch, type JSX, onMount } from "solid-js"
import { useLocal } from "@tui/context/local"
import { useTheme } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border"
@@ -425,6 +425,10 @@ export function Prompt(props: PromptProps) {
},
body: {
agent: local.agent.current().name,
model: {
providerID: local.model.current().providerID,
modelID: local.model.current().modelID,
},
command: inputText,
},
})
@@ -132,7 +132,16 @@ function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
if (c instanceof RGBA) return c
if (typeof c === "string") {
if (c === "transparent" || c === "none") return RGBA.fromInts(0, 0, 0, 0)
return c.startsWith("#") ? RGBA.fromHex(c) : resolveColor(defs[c])
if (c.startsWith("#")) return RGBA.fromHex(c)
if (defs[c]) {
return resolveColor(defs[c])
} else if (theme.theme[c as keyof Theme]) {
return resolveColor(theme.theme[c as keyof Theme])
} else {
throw new Error(`Color reference "${c}" not found in defs or theme`)
}
}
return resolveColor(c[mode])
}
@@ -54,6 +54,7 @@ import { DialogMessage } from "./dialog-message"
import type { PromptInfo } from "../../component/prompt/history"
import { iife } from "@/util/iife"
import { DialogConfirm } from "@tui/ui/dialog-confirm"
import { DialogPrompt } from "@tui/ui/dialog-prompt"
import { DialogTimeline } from "./dialog-timeline"
import { DialogSessionRename } from "../../component/dialog-session-rename"
import { Sidebar } from "./sidebar"
@@ -105,11 +106,6 @@ export function Session() {
return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
})
const lastUserMessage = createMemo(() => {
const p = pending()
return messages().findLast((x) => x.role === "user" && (!p || x.id < p)) as UserMessage
})
const dimensions = useTerminalDimensions()
const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">(kv.get("sidebar", "auto"))
const [conceal, setConceal] = createSignal(true)
@@ -583,12 +579,19 @@ export function Session() {
transcript += `---\n\n`
}
// Save to file in data directory
const exportDir = path.join(Global.Path.data, "exports")
await fs.mkdir(exportDir, { recursive: true })
// Prompt for optional filename
const customFilename = await DialogPrompt.show(
dialog,
"Export filename",
`session-${sessionData.id.slice(0, 8)}.md`,
)
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
const filename = `session-${sessionData.id.slice(0, 8)}-${timestamp}.md`
// Cancel if user pressed escape
if (customFilename === null) return
// Save to file in current working directory
const exportDir = process.cwd()
const filename = customFilename.trim()
const filepath = path.join(exportDir, filename)
await Bun.write(filepath, transcript)
@@ -982,51 +985,62 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
<text fg={theme.textMuted}>{props.message.error?.data.message}</text>
</box>
</Show>
<Show when={props.last && status().type !== "idle"}>
<box paddingLeft={3} flexDirection="row" gap={1} marginTop={1}>
<text fg={local.agent.color(props.message.mode)}>{Locale.titlecase(props.message.mode)}</text>
<Shimmer text={props.message.modelID} color={theme.text} />
{(() => {
const retry = createMemo(() => {
const s = status()
if (s.type !== "retry") return
return s
})
const [seconds, setSeconds] = createSignal(0)
onMount(() => {
const timer = setInterval(() => {
const next = retry()?.next
if (next) setSeconds(Math.round((next - Date.now()) / 1000))
}, 1000)
onCleanup(() => {
clearInterval(timer)
<Switch>
<Match when={props.last && status().type !== "idle"}>
<box paddingLeft={3} flexDirection="row" gap={1} marginTop={1}>
<text fg={local.agent.color(props.message.mode)}>{Locale.titlecase(props.message.mode)}</text>
<Shimmer text={props.message.modelID} color={theme.text} />
{(() => {
const retry = createMemo(() => {
const s = status()
if (s.type !== "retry") return
return s
})
})
return (
<Show when={retry()}>
<text fg={theme.error}>
{retry()!.message} [attempt #{retry()!.attempt}
{seconds() > 0 ? `, retrying in ${seconds()}s` : ""}]
</text>
</Show>
)
})()}
</box>
</Show>
<Show
when={
props.message.time.completed &&
props.parts.some((item) => item.type === "step-finish" && item.reason !== "tool-calls")
}
>
<box paddingLeft={3}>
<text marginTop={1}>
<span style={{ fg: local.agent.color(props.message.mode) }}>{Locale.titlecase(props.message.mode)}</span>{" "}
<span style={{ fg: theme.textMuted }}>{props.message.modelID}</span>
</text>
</box>
</Show>
const message = createMemo(() => {
const r = retry()
if (!r) return
if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
return "gemini 3 way too hot right now"
if (r.message.length > 50) return r.message.slice(0, 50) + "..."
return r.message
})
const [seconds, setSeconds] = createSignal(0)
onMount(() => {
const timer = setInterval(() => {
const next = retry()?.next
if (next) setSeconds(Math.round((next - Date.now()) / 1000))
}, 1000)
onCleanup(() => {
clearInterval(timer)
})
})
return (
<Show when={retry()}>
<text fg={theme.error}>
{message()} [retrying {seconds() > 0 ? `in ${seconds()}s ` : ""}
attempt #{retry()!.attempt}]
</text>
</Show>
)
})()}
</box>
</Match>
<Match
when={
(props.message.time.completed &&
props.parts.some((item) => item.type === "step-finish" && item.reason !== "tool-calls")) ||
props.last
}
>
<box paddingLeft={3}>
<text marginTop={1}>
<span style={{ fg: local.agent.color(props.message.mode) }}>{Locale.titlecase(props.message.mode)}</span>{" "}
<span style={{ fg: theme.textMuted }}>{props.message.modelID}</span>
</text>
</box>
</Match>
</Switch>
</>
)
}
@@ -23,7 +23,6 @@ export function Toast() {
alignItems="flex-start"
top={2}
right={2}
width={Math.min(60, dimensions().width - 6)}
maxWidth={Math.min(60, dimensions().width - 6)}
paddingLeft={2}
paddingRight={2}
@@ -39,7 +38,9 @@ export function Toast() {
{current().title}
</text>
</Show>
<text fg={theme.text}>{current().message}</text>
<text fg={theme.text} wrapMode="word" width="100%">
{current().message}
</text>
</box>
)}
</Show>
+31 -24
View File
@@ -542,36 +542,43 @@ export namespace Config {
.describe("Custom provider configurations and model overrides"),
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
formatter: z
.record(
z.string(),
z.object({
disabled: z.boolean().optional(),
command: z.array(z.string()).optional(),
environment: z.record(z.string(), z.string()).optional(),
extensions: z.array(z.string()).optional(),
}),
)
.union([
z.literal(false),
z.record(
z.string(),
z.object({
disabled: z.boolean().optional(),
command: z.array(z.string()).optional(),
environment: z.record(z.string(), z.string()).optional(),
extensions: z.array(z.string()).optional(),
}),
),
])
.optional(),
lsp: z
.record(
z.string(),
z.union([
z.object({
disabled: z.literal(true),
}),
z.object({
command: z.array(z.string()),
extensions: z.array(z.string()).optional(),
disabled: z.boolean().optional(),
env: z.record(z.string(), z.string()).optional(),
initialization: z.record(z.string(), z.any()).optional(),
}),
]),
)
.union([
z.literal(false),
z.record(
z.string(),
z.union([
z.object({
disabled: z.literal(true),
}),
z.object({
command: z.array(z.string()),
extensions: z.array(z.string()).optional(),
disabled: z.boolean().optional(),
env: z.record(z.string(), z.string()).optional(),
initialization: z.record(z.string(), z.any()).optional(),
}),
]),
),
])
.optional()
.refine(
(data) => {
if (!data) return true
if (typeof data === "boolean") return true
const serverIds = new Set(Object.values(LSPServer).map((s) => s.id))
return Object.entries(data).every(([id, config]) => {
+2
View File
@@ -11,6 +11,8 @@ export namespace Flag {
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH =
process.env["OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH"]
// Experimental
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
+8
View File
@@ -28,6 +28,14 @@ export namespace Format {
const cfg = await Config.get()
const formatters: Record<string, Formatter.Info> = {}
if (cfg.formatter === false) {
log.info("all formatters are disabled")
return {
enabled,
formatters,
}
}
for (const item of Object.values(Formatter)) {
formatters[item.name] = item
}
+12 -1
View File
@@ -62,10 +62,21 @@ export namespace LSP {
async () => {
const clients: LSPClient.Info[] = []
const servers: Record<string, LSPServer.Info> = {}
const cfg = await Config.get()
if (cfg.lsp === false) {
log.info("all LSPs are disabled")
return {
broken: new Set<string>(),
servers,
clients,
spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
}
}
for (const server of Object.values(LSPServer)) {
servers[server.id] = server
}
const cfg = await Config.get()
for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
const existing = servers[name]
if (item.disabled) {
+96 -8
View File
@@ -250,12 +250,12 @@ export namespace LSPServer {
},
}
export const RubyLsp: Info = {
export const Rubocop: Info = {
id: "ruby-lsp",
root: NearestRoot(["Gemfile"]),
extensions: [".rb", ".rake", ".gemspec", ".ru"],
async spawn(root) {
let bin = Bun.which("ruby-lsp", {
let bin = Bun.which("rubocop", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
if (!bin) {
@@ -266,25 +266,25 @@ export namespace LSPServer {
return
}
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("installing ruby-lsp")
log.info("installing rubocop")
const proc = Bun.spawn({
cmd: ["gem", "install", "ruby-lsp", "--bindir", Global.Path.bin],
cmd: ["gem", "install", "rubocop", "--bindir", Global.Path.bin],
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
log.error("Failed to install ruby-lsp")
log.error("Failed to install rubocop")
return
}
bin = path.join(Global.Path.bin, "ruby-lsp" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed ruby-lsp`, {
bin = path.join(Global.Path.bin, "rubocop" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed rubocop`, {
bin,
})
}
return {
process: spawn(bin!, ["--stdio"], {
process: spawn(bin!, ["--lsp"], {
cwd: root,
}),
}
@@ -945,6 +945,54 @@ export namespace LSPServer {
},
}
export const YamlLS: Info = {
id: "yaml-ls",
extensions: [".yaml", ".yml"],
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
async spawn(root) {
let binary = Bun.which("yaml-language-server")
const args: string[] = []
if (!binary) {
const js = path.join(
Global.Path.bin,
"node_modules",
"yaml-language-server",
"out",
"server",
"src",
"server.js",
)
const exists = await Bun.file(js).exists()
if (!exists) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], {
cwd: Global.Path.bin,
env: {
...process.env,
BUN_BE_BUN: "1",
},
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
}).exited
}
binary = BunProc.which()
args.push("run", js)
}
args.push("--stdio")
const proc = spawn(binary, args, {
cwd: root,
env: {
...process.env,
BUN_BE_BUN: "1",
},
})
return {
process: proc,
}
},
}
export const LuaLS: Info = {
id: "lua-ls",
root: NearestRoot([
@@ -1077,4 +1125,44 @@ export namespace LSPServer {
}
},
}
export const PHPIntelephense: Info = {
id: "php intelephense",
extensions: [".php"],
root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
async spawn(root) {
let binary = Bun.which("intelephense")
const args: string[] = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "intelephense"], {
cwd: Global.Path.bin,
env: {
...process.env,
BUN_BE_BUN: "1",
},
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
}).exited
}
binary = BunProc.which()
args.push("run", js)
}
args.push("--stdio")
const proc = spawn(binary, args, {
cwd: root,
env: {
...process.env,
BUN_BE_BUN: "1",
},
})
return {
process: proc,
initialization: {},
}
},
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { experimental_createMCPClient, type Tool } from "ai"
import { type Tool } from "ai"
import { experimental_createMCPClient } from "@ai-sdk/mcp"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
+6 -1
View File
@@ -186,8 +186,13 @@ export namespace Permission {
public readonly permissionID: string,
public readonly toolCallID?: string,
public readonly metadata?: Record<string, any>,
public readonly reason?: string,
) {
super(`The user rejected permission to use this specific tool call. You may try again with different parameters.`)
super(
reason !== undefined
? reason
: `The user rejected permission to use this specific tool call. You may try again with different parameters.`,
)
}
}
}
+17 -13
View File
@@ -494,6 +494,7 @@ export namespace Provider {
if (pkg.includes("@ai-sdk/openai-compatible") && options["includeUsage"] === undefined) {
options["includeUsage"] = true
}
const key = Bun.hash.xxHash32(JSON.stringify({ pkg, options }))
const existing = s.sdk.get(key)
if (existing) return existing
@@ -515,26 +516,29 @@ export namespace Provider {
const modPath =
provider.id === "google-vertex-anthropic" ? `${installedPath}/dist/anthropic/index.mjs` : installedPath
const mod = await import(modPath)
if (options["timeout"] !== undefined && options["timeout"] !== null) {
// Preserve custom fetch if it exists, wrap it with timeout logic
const customFetch = options["fetch"]
options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
const { signal, ...rest } = init ?? {}
const customFetch = options["fetch"]
options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
// Preserve custom fetch if it exists, wrap it with timeout logic
const fetchFn = customFetch ?? fetch
const opts = init ?? {}
if (options["timeout"] !== undefined && options["timeout"] !== null) {
const signals: AbortSignal[] = []
if (signal) signals.push(signal)
if (opts.signal) signals.push(opts.signal)
if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]
const fetchFn = customFetch ?? fetch
return fetchFn(input, {
...rest,
signal: combined,
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
timeout: false,
})
opts.signal = combined
}
return fetchFn(input, {
...opts,
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
timeout: false,
})
}
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
const loaded = fn({
+21 -1
View File
@@ -24,7 +24,6 @@ export namespace ProviderTransform {
const result: ModelMessage[] = []
for (let i = 0; i < msgs.length; i++) {
const msg = msgs[i]
const prevMsg = msgs[i - 1]
const nextMsg = msgs[i + 1]
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
@@ -137,10 +136,23 @@ export namespace ProviderTransform {
): Record<string, any> | undefined {
const result: Record<string, any> = {}
// switch to providerID later, for now use this
if (npm === "@openrouter/ai-sdk-provider") {
result["usage"] = {
include: true,
}
}
if (providerID === "openai") {
result["promptCacheKey"] = sessionID
}
if (providerID === "google") {
result["thinkingConfig"] = {
includeThoughts: true,
}
}
if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
if (modelID.includes("codex")) {
result["store"] = false
@@ -178,10 +190,18 @@ export namespace ProviderTransform {
return {
["anthropic" as string]: options,
}
case "@ai-sdk/google":
return {
["google" as string]: options,
}
case "@ai-sdk/gateway":
return {
["gateway" as string]: options,
}
case "@openrouter/ai-sdk-provider":
return {
["openrouter" as string]: options,
}
default:
return {
[providerID]: options,
+3
View File
@@ -42,6 +42,9 @@ import { SessionSummary } from "@/session/summary"
import { GlobalBus } from "@/bus/global"
import { SessionStatus } from "@/session/status"
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
globalThis.AI_SDK_LOG_WARNINGS = false
const ERRORS = {
400: {
description: "Bad request",
+25 -17
View File
@@ -382,17 +382,23 @@ export namespace Session {
const adjustedInputTokens = excludesCachedTokens
? (input.usage.inputTokens ?? 0)
: (input.usage.inputTokens ?? 0) - cachedInputTokens
const safe = (value: number) => {
if (!Number.isFinite(value)) return 0
return value
}
const tokens = {
input: adjustedInputTokens,
output: input.usage.outputTokens ?? 0,
reasoning: input.usage?.reasoningTokens ?? 0,
input: safe(adjustedInputTokens),
output: safe(input.usage.outputTokens ?? 0),
reasoning: safe(input.usage?.reasoningTokens ?? 0),
cache: {
write: (input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
// @ts-expect-error
input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
0) as number,
read: cachedInputTokens,
write: safe(
(input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
// @ts-expect-error
input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
0) as number,
),
read: safe(cachedInputTokens),
},
}
@@ -401,15 +407,17 @@ export namespace Session {
? input.model.cost.context_over_200k
: input.model.cost
return {
cost: new Decimal(0)
.add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000))
.add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.read).mul(costInfo?.cache_read ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.write).mul(costInfo?.cache_write ?? 0).div(1_000_000))
// TODO: update models.dev to have better pricing model, for now:
// charge reasoning tokens at the same rate as output tokens
.add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))
.toNumber(),
cost: safe(
new Decimal(0)
.add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000))
.add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.read).mul(costInfo?.cache_read ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.write).mul(costInfo?.cache_write ?? 0).div(1_000_000))
// TODO: update models.dev to have better pricing model, for now:
// charge reasoning tokens at the same rate as output tokens
.add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))
.toNumber(),
),
tokens,
}
},
@@ -136,6 +136,7 @@ export namespace SessionProcessor {
const parts = await MessageV2.parts(input.assistantMessage.id)
const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD)
if (
lastThree.length === DOOM_LOOP_THRESHOLD &&
lastThree.every(
@@ -160,6 +161,17 @@ export namespace SessionProcessor {
input: value.input,
},
})
} else if (permission.doom_loop === "deny") {
throw new Permission.RejectedError(
input.assistantMessage.sessionID,
"doom_loop",
value.toolCallId,
{
tool: value.toolName,
input: value.input,
},
`You seem to be stuck in a doom loop, please stop repeating the same action`,
)
}
}
}
+19 -17
View File
@@ -600,12 +600,9 @@ export namespace SessionPrompt {
throw new Error("Impossible")
})
async function resolveModel(input: { model: PromptInput["model"]; agent: Agent.Info }) {
if (input.model) {
return input.model
}
if (input.agent.model) {
return input.agent.model
async function lastModel(sessionID: string) {
for await (const item of MessageV2.stream(sessionID)) {
if (item.info.role === "user" && item.info.model) return item.info.model
}
return Provider.defaultModel()
}
@@ -794,10 +791,7 @@ export namespace SessionPrompt {
tools: input.tools,
system: input.system,
agent: agent.name,
model: await resolveModel({
model: input.model,
agent,
}),
model: input.model ?? agent.model ?? (await lastModel(input.sessionID)),
}
const parts = await Promise.all(
@@ -899,7 +893,7 @@ export namespace SessionPrompt {
abort: new AbortController().signal,
agent: input.agent!,
messageID: info.id,
extra: { bypassCwdCheck: true },
extra: { bypassCwdCheck: true, ...info.model },
metadata: async () => {},
})
pieces.push(
@@ -1091,6 +1085,12 @@ export namespace SessionPrompt {
export const ShellInput = z.object({
sessionID: Identifier.schema("session"),
agent: z.string(),
model: z
.object({
providerID: z.string(),
modelID: z.string(),
})
.optional(),
command: z.string(),
})
export type ShellInput = z.infer<typeof ShellInput>
@@ -1100,7 +1100,7 @@ export namespace SessionPrompt {
SessionRevert.cleanup(session)
}
const agent = await Agent.get(input.agent)
const model = await resolveModel({ agent, model: undefined })
const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))
const userMsg: MessageV2.User = {
id: Identifier.ascending("message"),
sessionID: input.sessionID,
@@ -1338,10 +1338,8 @@ export namespace SessionPrompt {
return cmdAgent.model
}
}
if (input.model) {
return Provider.parseModel(input.model)
}
return await Provider.defaultModel()
if (input.model) return Provider.parseModel(input.model)
return await lastModel(input.sessionID)
})()
const agent = await Agent.get(agentName)
@@ -1397,7 +1395,11 @@ export namespace SessionPrompt {
...small.info.options,
}
if (small.providerID === "openai" || small.modelID.includes("gpt-5")) {
options["reasoningEffort"] = "minimal"
if (small.modelID.includes("5.1")) {
options["reasoningEffort"] = "low"
} else {
options["reasoningEffort"] = "minimal"
}
}
if (small.providerID === "google") {
options["thinkingConfig"] = {
+34 -6
View File
@@ -12,8 +12,14 @@ import { Filesystem } from "@/util/filesystem"
import { Wildcard } from "@/util/wildcard"
import { Permission } from "@/permission"
import { fileURLToPath } from "url"
import { Flag } from "@/flag/flag.ts"
import path from "path"
const MAX_OUTPUT_LENGTH = 30_000
const DEFAULT_MAX_OUTPUT_LENGTH = 30_000
const MAX_OUTPUT_LENGTH = (() => {
const parsed = Number(Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH)
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_OUTPUT_LENGTH
})()
const DEFAULT_TIMEOUT = 1 * 60 * 1000
const MAX_TIMEOUT = 10 * 60 * 1000
const SIGKILL_TIMEOUT_MS = 200
@@ -68,7 +74,8 @@ export const BashTool = Tool.define("bash", {
if (!tree) {
throw new Error("Failed to parse command")
}
const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash)
const agent = await Agent.get(ctx.agent)
const permissions = agent.permission.bash
const askPatterns = new Set<string>()
for (const node of tree.rootNode.descendantsOfType("command")) {
@@ -107,9 +114,30 @@ export const BashTool = Tool.define("bash", {
: resolved
if (!Filesystem.contains(Instance.directory, normalized)) {
throw new Error(
`This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
)
const parentDir = path.dirname(normalized)
if (agent.permission.external_directory === "ask") {
await Permission.ask({
type: "external_directory",
pattern: [parentDir, path.join(parentDir, "*")],
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: `This command references paths outside of ${Instance.directory}`,
metadata: {
command: params.command,
},
})
} else if (agent.permission.external_directory === "deny") {
throw new Permission.RejectedError(
ctx.sessionID,
"external_directory",
ctx.callID,
{
command: params.command,
},
`This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
)
}
}
}
}
@@ -271,7 +299,7 @@ export const BashTool = Tool.define("bash", {
}
return {
title: params.command,
title: params.description,
metadata: {
output,
exit: proc.exitCode,
+1 -1
View File
@@ -35,7 +35,7 @@ Usage notes:
# Committing changes with git
When the user asks you to create a new git commit, follow these steps carefully:
If and only if the user asks you to create a new git commit, follow these steps carefully:
1. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following bash commands in parallel, each using the Bash tool:
- Run a git status command to see all untracked files.
+12 -1
View File
@@ -47,7 +47,7 @@ export const EditTool = Tool.define("edit", {
if (agent.permission.external_directory === "ask") {
await Permission.ask({
type: "external_directory",
pattern: parentDir,
pattern: [parentDir, path.join(parentDir, "*")],
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
@@ -57,6 +57,17 @@ export const EditTool = Tool.define("edit", {
parentDir,
},
})
} else if (agent.permission.external_directory === "deny") {
throw new Permission.RejectedError(
ctx.sessionID,
"external_directory",
ctx.callID,
{
filepath: filePath,
parentDir,
},
`File ${filePath} is not in the current working directory`,
)
}
}
+12 -1
View File
@@ -58,7 +58,7 @@ export const PatchTool = Tool.define("patch", {
if (agent.permission.external_directory === "ask") {
await Permission.ask({
type: "external_directory",
pattern: parentDir,
pattern: [parentDir, path.join(parentDir, "*")],
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
@@ -68,6 +68,17 @@ export const PatchTool = Tool.define("patch", {
parentDir,
},
})
} else if (agent.permission.external_directory === "deny") {
throw new Permission.RejectedError(
ctx.sessionID,
"external_directory",
ctx.callID,
{
filepath: filePath,
parentDir,
},
`File ${filePath} is not in the current working directory`,
)
}
}
+12 -1
View File
@@ -36,7 +36,7 @@ export const ReadTool = Tool.define("read", {
if (agent.permission.external_directory === "ask") {
await Permission.ask({
type: "external_directory",
pattern: parentDir,
pattern: [parentDir, path.join(parentDir, "*")],
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
@@ -46,6 +46,17 @@ export const ReadTool = Tool.define("read", {
parentDir,
},
})
} else if (agent.permission.external_directory === "deny") {
throw new Permission.RejectedError(
ctx.sessionID,
"external_directory",
ctx.callID,
{
filepath: filepath,
parentDir,
},
`File ${filepath} is not in the current working directory`,
)
}
}
+17 -4
View File
@@ -7,6 +7,7 @@ import { MessageV2 } from "../session/message-v2"
import { Identifier } from "../id/id"
import { Agent } from "../agent/agent"
import { SessionPrompt } from "../session/prompt"
import { iife } from "@/util/iife"
import { defer } from "@/util/defer"
export const TaskTool = Tool.define("task", async () => {
@@ -23,13 +24,21 @@ export const TaskTool = Tool.define("task", async () => {
description: z.string().describe("A short (3-5 words) description of the task"),
prompt: z.string().describe("The task for the agent to perform"),
subagent_type: z.string().describe("The type of specialized agent to use for this task"),
session_id: z.string().describe("Existing Task session to continue").optional(),
}),
async execute(params, ctx) {
const agent = await Agent.get(params.subagent_type)
if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)
const session = await Session.create({
parentID: ctx.sessionID,
title: params.description + ` (@${agent.name} subagent)`,
const session = await iife(async () => {
if (params.session_id) {
const found = await Session.get(params.session_id).catch(() => {})
if (found) return found
}
return await Session.create({
parentID: ctx.sessionID,
title: params.description + ` (@${agent.name} subagent)`,
})
})
const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID })
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
@@ -89,13 +98,17 @@ export const TaskTool = Tool.define("task", async () => {
all = await Session.messages({ sessionID: session.id })
all = all.filter((x) => x.info.role === "assistant")
all = all.flatMap((msg) => msg.parts.filter((x: any) => x.type === "tool") as MessageV2.ToolPart[])
const text = result.parts.findLast((x) => x.type === "text")?.text ?? ""
const output = text + "\n\n" + ["<task_metadata>", `session_id: ${session.id}`, "</task_metadata>"].join("\n")
return {
title: params.description,
metadata: {
summary: all,
sessionId: session.id,
},
output: (result.parts.findLast((x: any) => x.type === "text") as any)?.text ?? "",
output,
}
},
}
+1 -1
View File
@@ -18,7 +18,7 @@ When NOT to use the Task tool:
Usage notes:
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
3. Each agent invocation is stateless unless you provide a session_id. Your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
4. The agent's outputs should generally be trusted
5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
+12 -1
View File
@@ -26,7 +26,7 @@ export const WriteTool = Tool.define("write", {
if (agent.permission.external_directory === "ask") {
await Permission.ask({
type: "external_directory",
pattern: parentDir,
pattern: [parentDir, path.join(parentDir, "*")],
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
@@ -36,6 +36,17 @@ export const WriteTool = Tool.define("write", {
parentDir,
},
})
} else if (agent.permission.external_directory === "deny") {
throw new Permission.RejectedError(
ctx.sessionID,
"external_directory",
ctx.callID,
{
filepath: filepath,
parentDir,
},
`File ${filepath} is not in the current working directory`,
)
}
}
+19 -16
View File
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
import path from "path"
import { BashTool } from "../../src/tool/bash"
import { Instance } from "../../src/project/instance"
import { Permission } from "../../src/permission"
const ctx = {
sessionID: "test",
@@ -33,20 +34,22 @@ describe("tool.bash", () => {
})
})
test("cd ../ should fail outside of project root", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
expect(
bash.execute(
{
command: "cd ../",
description: "Try to cd to parent directory",
},
ctx,
),
).rejects.toThrow("This command references paths outside of")
},
})
})
// TODO: better test
// test("cd ../ should ask for permission for external directory", async () => {
// await Instance.provide({
// directory: projectRoot,
// fn: async () => {
// bash.execute(
// {
// command: "cd ../",
// description: "Try to cd to parent directory",
// },
// ctx,
// )
// // Give time for permission to be asked
// await new Promise((resolve) => setTimeout(resolve, 1000))
// expect(Permission.pending()[ctx.sessionID]).toBeDefined()
// },
// })
// })
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.76",
"version": "1.0.81",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.76",
"version": "1.0.81",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+31 -23
View File
@@ -1117,33 +1117,37 @@ export type Config = {
mcp?: {
[key: string]: McpLocalConfig | McpRemoteConfig
}
formatter?: {
[key: string]: {
disabled?: boolean
command?: Array<string>
environment?: {
[key: string]: string
}
extensions?: Array<string>
}
}
lsp?: {
[key: string]:
| {
disabled: true
}
| {
command: Array<string>
extensions?: Array<string>
formatter?:
| false
| {
[key: string]: {
disabled?: boolean
env?: {
command?: Array<string>
environment?: {
[key: string]: string
}
initialization?: {
[key: string]: unknown
}
extensions?: Array<string>
}
}
}
lsp?:
| false
| {
[key: string]:
| {
disabled: true
}
| {
command: Array<string>
extensions?: Array<string>
disabled?: boolean
env?: {
[key: string]: string
}
initialization?: {
[key: string]: unknown
}
}
}
/**
* Additional instruction files or patterns to include
*/
@@ -2290,6 +2294,10 @@ export type SessionCommandResponse = SessionCommandResponses[keyof SessionComman
export type SessionShellData = {
body?: {
agent: string
model?: {
providerID: string
modelID: string
}
command: string
}
path: {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.76",
"version": "1.0.81",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.76",
"version": "1.0.81",
"type": "module",
"exports": {
".": "./src/components/index.ts",
+12 -6
View File
@@ -1,8 +1,7 @@
[data-component="markdown"] {
min-width: 0;
max-width: 100%;
overflow: auto;
scrollbar-width: none;
overflow: hidden;
color: var(--text-base);
text-wrap: pretty;
@@ -14,10 +13,6 @@
line-height: var(--line-height-large); /* 166.667% */
letter-spacing: var(--letter-spacing-normal);
&::-webkit-scrollbar {
display: none;
}
h1,
h2,
h3 {
@@ -41,4 +36,15 @@
margin-bottom: 16px;
border-color: var(--border-weaker-base);
}
pre {
margin-top: 2rem;
margin-bottom: 2rem;
overflow: auto;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
}
@@ -22,6 +22,8 @@
}
[data-component="text-part"] {
width: 100%;
[data-component="markdown"] {
margin-top: 32px;
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "@opencode-ai/util",
"version": "1.0.81",
"private": true,
"type": "module",
"exports": {
"./*": "./src/*.ts"
},
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"zod": "catalog:"
},
"devDependencies": {
"typescript": "catalog:"
}
}
+11
View File
@@ -0,0 +1,11 @@
import { z } from "zod"
export function fn<T extends z.ZodType, Result>(schema: T, cb: (input: z.infer<T>) => Result) {
const result = (input: z.infer<T>) => {
const parsed = schema.parse(input)
return cb(parsed)
}
result.force = (input: z.infer<T>) => cb(input)
result.schema = schema
return result
}
+3
View File
@@ -0,0 +1,3 @@
export function iife<T>(fn: () => T) {
return fn()
}
+11
View File
@@ -0,0 +1,11 @@
export function lazy<T>(fn: () => T) {
let value: T | undefined
let loaded = false
return (): T => {
if (loaded) return value as T
loaded = true
value = fn()
return value as T
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,
"noEmit": true,
"strict": true,
"isolatedModules": true
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.76",
"version": "1.0.81",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+34 -19
View File
@@ -11,25 +11,27 @@ OpenCode integrates with your Language Server Protocol (LSP) to help the LLM int
OpenCode comes with several built-in LSP servers for popular languages:
| LSP Server | Extensions | Requirements |
| ------------- | ---------------------------------------------------- | ------------------------------------------------------------ |
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` dependency in project |
| deno | .ts, .tsx, .js, .jsx, .mjs | `deno` command available (auto-detects deno.json/deno.jsonc) |
| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue | `eslint` dependency in project |
| gopls | .go | `go` command available |
| ruby-lsp | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
| pyright | .py, .pyi | `pyright` dependency installed |
| elixir-ls | .ex, .exs | `elixir` command available |
| zls | .zig, .zon | `zig` command available |
| csharp | .cs | `.NET SDK` installed |
| vue | .vue | Auto-installs for Vue projects |
| rust | .rs | `rust-analyzer` command available |
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects |
| svelte | .svelte | Auto-installs for Svelte projects |
| astro | .astro | Auto-installs for Astro projects |
| jdtls | .java | `Java SDK (version 21+)` installed |
| lua-ls | .lua | Auto-installs for Lua projects |
| sourcekit-lsp | .swift, .objc, .objcpp | `swift` installed (`xcode` on macOS) |
| LSP Server | Extensions | Requirements |
| ------------------ | ---------------------------------------------------- | ------------------------------------------------------------ |
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` dependency in project |
| deno | .ts, .tsx, .js, .jsx, .mjs | `deno` command available (auto-detects deno.json/deno.jsonc) |
| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue | `eslint` dependency in project |
| gopls | .go | `go` command available |
| ruby-lsp (rubocop) | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
| pyright | .py, .pyi | `pyright` dependency installed |
| elixir-ls | .ex, .exs | `elixir` command available |
| zls | .zig, .zon | `zig` command available |
| csharp | .cs | `.NET SDK` installed |
| vue | .vue | Auto-installs for Vue projects |
| rust | .rs | `rust-analyzer` command available |
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects |
| svelte | .svelte | Auto-installs for Svelte projects |
| astro | .astro | Auto-installs for Astro projects |
| yaml-ls | .yaml, .yml | Auto-installs Red Hat yaml-language-server |
| jdtls | .java | `Java SDK (version 21+)` installed |
| lua-ls | .lua | Auto-installs for Lua projects |
| sourcekit-lsp | .swift, .objc, .objcpp | `swift` installed (`xcode` on macOS) |
| php intelephense | .php | Auto-installs for PHP projects |
LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
@@ -105,3 +107,16 @@ You can add custom LSP servers by specifying the command and file extensions:
}
}
```
---
## Additional Information
### PHP Intelephense
PHP Intelephense offers premium features through a license key. You can provide a license key by placing (only) the key in a text file at:
- On macOS/Linux: `$HOME/intelephense/licence.txt`
- On Windows: `%USERPROFILE%/intelephense/licence.txt`
The file should contain only the license key with no additional content.
+66
View File
@@ -61,6 +61,72 @@ export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree
---
### Events
Plugins can subscribe to events as seen below in the Examples section. Here is a list of the different events available.
#### Command Events
- `command.executed`
#### File Events
- `file.edited`
- `file.watcher.updated`
#### Installation Events
- `installation.updated`
#### LSP Events
- `lsp.client.diagnostics`
- `lsp.updated`
#### Message Events
- `message.part.removed`
- `message.part.updated`
- `message.removed`
- `message.updated`
#### Permission Events
- `permission.replied`
- `permission.updated`
#### Server Events
- `server.connected`
#### Session Events
- `session.created`
- `session.compacted`
- `session.deleted`
- `session.diff`
- `session.error`
- `session.idle`
- `session.status`
- `session.updated`
#### Todo Events
- `todo.updated`
#### Tool Events
- `tool.execute.after`
- `tool.execute.before`
#### TUI Events
- `tui.prompt.append`
- `tui.command.execute`
- `tui.toast.show`
---
## Examples
Here are some examples of plugins you can use to extend opencode.
@@ -357,6 +357,42 @@ Or if you already have an API key, you can select **Manually enter API Key** and
---
### Cortecs
1. Head over to the [Cortecs console](https://cortecs.ai/), create an account, and generate an API key.
2. Run `opencode auth login` and select **Cortecs**.
```bash
$ opencode auth login
┌ Add credential
◆ Select provider
│ ● Cortecs
│ ...
```
3. Enter your Cortecs API key.
```bash
$ opencode auth login
┌ Add credential
◇ Select provider
│ Cortecs
◇ Enter your API key
│ _
```
4. Run the `/models` command to select a model like _Kimi K2 Instruct_.
---
### DeepSeek
1. Head over to the [DeepSeek console](https://platform.deepseek.com/), create an account, and click **Create new API key**.
+20 -17
View File
@@ -62,23 +62,24 @@ You are charged per request and you can add credits to your account.
You can also access our models through the following API endpoints.
| Model | Model ID | Endpoint | AI SDK Package |
| ----------------- | ----------------- | --------------------------------------------- | --------------------------- |
| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Grok Code Fast 1 | grok-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Model | Model ID | Endpoint | AI SDK Package |
| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- |
| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` |
| GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Grok Code Fast 1 | grok-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
The [model id](/docs/config/#models) in your OpenCode config
uses the format `opencode/<model-id>`. For example, for GPT 5.1 Codex, you would
@@ -130,6 +131,8 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
| Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 |
| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 |
| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 |
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| GPT 5.1 | $1.25 | $10.00 | $0.125 | - |
| GPT 5.1 Codex | $1.25 | $10.00 | $0.125 | - |
| GPT 5 | $1.25 | $10.00 | $0.125 | - |
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.76",
"version": "1.0.81",
"publisher": "sst-dev",
"repository": {
"type": "git",