Compare commits

...

42 Commits

Author SHA1 Message Date
opencode a7774115c5 release: v1.0.62 2025-11-13 00:13:18 +00:00
Luke Parker 288bc88e40 fix: Tool calling on windows (#4234) 2025-11-12 17:47:39 -06:00
Aiden Cline 6d36dbf9de fix: github action dirty check (#4262) 2025-11-12 16:16:07 -06:00
OpeOginni 4ab4baf3a4 feat(sidebar): add expandable sections for sidebar (#4132)
Co-authored-by: GitHub Action <action@github.com>
2025-11-12 16:15:17 -06:00
phantomreactor 90f05eb9c2 paste images in wsl using ctrl+v (#4123)
Co-authored-by: GitHub Action <action@github.com>
2025-11-12 15:10:23 -06:00
Melih Mucuk b63b6d04c6 Fix usage & billing for custom model aliases and cached/reasoning tokens (#4222)
Co-authored-by: Melih Mucuk <melih@monkeysteam.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-12 13:59:35 -06:00
Aiden Cline 8addaa7e08 fix: custom model name merging 2025-11-12 13:55:13 -06:00
Elias a96bf8e62d docs: OVHcloud AI Endpoints provider (#4257) 2025-11-12 13:28:35 -06:00
Ivan c8bda598f5 fix: correct cache cost for OpenRouter and other OpenAI-compatible providers (#4256) 2025-11-12 12:41:44 -06:00
Adam c857cff585 fix(desktop): double listing dir 2025-11-12 12:17:54 -06:00
Aiden Cline fd9d2db755 ci: update zed sync 2025-11-12 10:52:20 -06:00
Aiden Cline b19fd14f80 ignore: make issue button send opencode version too 2025-11-12 10:40:48 -06:00
Sebastian Herrlinger a0f469095c upgrade opentui to 0.1.42, fixing some CJK/grapheme issues with prompt extmarks and char corruption 2025-11-12 15:35:16 +01:00
Adam 0ccb26df94 feat(desktop): sticky diff headers 2025-11-12 07:03:39 -06:00
Adam 71fd5966ad fix(desktop): styling tweaks 2025-11-12 07:03:38 -06:00
GitHub Action c02230de4f ignore: update download stats 2025-11-12 2025-11-12 12:05:15 +00:00
Filip aa2e2c76c0 fix: clangd hanging fixed (#3611)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
2025-11-12 00:21:55 -06:00
opencode 7c2d4ee79a release: v1.0.61 2025-11-12 03:10:55 +00:00
Dax Raad e3a2728fa3 tui: add double-esc interrupt mechanism for long-running operations
Users can now press escape twice within 5 seconds to interrupt long-running
operations in the TUI. The first press shows a visual hint, and the second
press aborts the current session.
2025-11-11 22:04:00 -05:00
Boston Cartwright 18260b037b feat: add SourceKit LSP support (#1545)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-11 20:51:33 -06:00
Dax Raad ad83dd3ad9 tui: fix autocomplete display to prevent long file paths from breaking layout 2025-11-12 02:36:43 +00:00
opencode 6f37315cd1 release: v1.0.60 2025-11-12 02:36:42 +00:00
Dax d81dce6a82 fix: add support for loading custom themes from .opencode/themes directory (#4229)
Co-authored-by: GitHub Action <action@github.com>
2025-11-11 21:30:38 -05:00
opencode 0bd11e970b release: v1.0.59 2025-11-12 02:07:41 +00:00
Dax Raad 7e29e1dd23 better errors on initial tui boot 2025-11-11 21:01:45 -05:00
Rafał Krzyważnia 491a2adf8d fix: resolve @file references in slash commands with subagents (#4221) 2025-11-11 19:38:50 -06:00
Aiden Cline c07d6487a8 fix config ordering (#4228) 2025-11-11 19:27:34 -06:00
Aiden Cline 9990e84d37 fix: ensure revert dialog moves that prompt to input box (#4227) 2025-11-11 19:08:59 -06:00
Aiden Cline 0b86adbe99 feat: agent color cfg (#4226)
Co-authored-by: 0xrin <0xrin1@protonmail.com>
Co-authored-by: GitHub Action <action@github.com>
2025-11-11 18:32:44 -06:00
Frank 834a2c09d5 wip: poc pr command 2025-11-11 18:50:28 -05:00
Frank f13c17e654 wip: poc pr command 2025-11-11 18:50:28 -05:00
Julian LaNeve a0611d92e4 docs: Update config references to latest Sonnet & Haiku models (#4210) 2025-11-11 16:52:45 -06:00
Aiden Cline 0b001c3e80 tweak: make todos appear list of modified files 2025-11-11 16:05:23 -06:00
Sebastian Herrlinger 53b7cb62c4 upgrade opentui to 0.1.41:
- enables modifyOtherKeys to get CSI u sequences in terminals that support it
- uses Private Mode 2026 for synced rendering to fix cursor flickering in terminals like iTerm2
- lazy highlighting for code renderables (perf)
- linear scroll acceleration by default
- align textarea default bindings more with readline
- fix vertical cursor movement in textarea
- introduce stdin buffer to handle chunked sequences
- improve capability detection (async)
- renderer emits focus/blur events when app is focused/blurred (if supported by terminal)
2025-11-11 23:00:31 +01:00
Aiden Cline c5e096c76a fix: costs being 0 when using custom model id overrides (#4219) 2025-11-11 15:58:14 -06:00
Aiden Cline e1fc4a756b Hide /share if disabled (#4215) 2025-11-11 14:47:39 -06:00
Aiden Cline e5bc4cbbcf ci: update changelog script 2025-11-11 14:27:13 -06:00
GitHub Action 459d5ec19b chore: format code 2025-11-11 20:21:00 +00:00
Aiden Cline 8baa222621 ci: update script 2025-11-11 14:20:19 -06:00
Dax Raad ce1397cc34 core: add test to verify OpenCode doesn't crash when starting in git repositories with no commit history 2025-11-11 20:17:36 +00:00
Ron Suhodrev dc7c5ced4c tui: restore full text when editing prompts with summarized content (#4030) 2025-11-11 20:17:36 +00:00
Corwin Marsh b8e8fe7e31 docs: Update dead Context7 mcp server link (#4207)
Co-authored-by: Corwin Marsh <corwinm@users.noreply.github.com>
2025-11-11 20:17:36 +00:00
56 changed files with 1366 additions and 440 deletions
+223
View File
@@ -0,0 +1,223 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"nord0": "#2E3440",
"nord1": "#3B4252",
"nord2": "#434C5E",
"nord3": "#4C566A",
"nord4": "#D8DEE9",
"nord5": "#E5E9F0",
"nord6": "#ECEFF4",
"nord7": "#8FBCBB",
"nord8": "#88C0D0",
"nord9": "#81A1C1",
"nord10": "#5E81AC",
"nord11": "#BF616A",
"nord12": "#D08770",
"nord13": "#EBCB8B",
"nord14": "#A3BE8C",
"nord15": "#B48EAD"
},
"theme": {
"primary": {
"dark": "nord8",
"light": "nord10"
},
"secondary": {
"dark": "nord9",
"light": "nord9"
},
"accent": {
"dark": "nord7",
"light": "nord7"
},
"error": {
"dark": "nord11",
"light": "nord11"
},
"warning": {
"dark": "nord12",
"light": "nord12"
},
"success": {
"dark": "nord14",
"light": "nord14"
},
"info": {
"dark": "nord8",
"light": "nord10"
},
"text": {
"dark": "nord4",
"light": "nord0"
},
"textMuted": {
"dark": "nord3",
"light": "nord1"
},
"background": {
"dark": "nord0",
"light": "nord6"
},
"backgroundPanel": {
"dark": "nord1",
"light": "nord5"
},
"backgroundElement": {
"dark": "nord1",
"light": "nord4"
},
"border": {
"dark": "nord2",
"light": "nord3"
},
"borderActive": {
"dark": "nord3",
"light": "nord2"
},
"borderSubtle": {
"dark": "nord2",
"light": "nord3"
},
"diffAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffContext": {
"dark": "nord3",
"light": "nord3"
},
"diffHunkHeader": {
"dark": "nord3",
"light": "nord3"
},
"diffHighlightAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffHighlightRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffAddedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffContextBg": {
"dark": "nord1",
"light": "nord5"
},
"diffLineNumber": {
"dark": "nord2",
"light": "nord4"
},
"diffAddedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"markdownText": {
"dark": "nord4",
"light": "nord0"
},
"markdownHeading": {
"dark": "nord8",
"light": "nord10"
},
"markdownLink": {
"dark": "nord9",
"light": "nord9"
},
"markdownLinkText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCode": {
"dark": "nord14",
"light": "nord14"
},
"markdownBlockQuote": {
"dark": "nord3",
"light": "nord3"
},
"markdownEmph": {
"dark": "nord12",
"light": "nord12"
},
"markdownStrong": {
"dark": "nord13",
"light": "nord13"
},
"markdownHorizontalRule": {
"dark": "nord3",
"light": "nord3"
},
"markdownListItem": {
"dark": "nord8",
"light": "nord10"
},
"markdownListEnumeration": {
"dark": "nord7",
"light": "nord7"
},
"markdownImage": {
"dark": "nord9",
"light": "nord9"
},
"markdownImageText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCodeBlock": {
"dark": "nord4",
"light": "nord0"
},
"syntaxComment": {
"dark": "nord3",
"light": "nord3"
},
"syntaxKeyword": {
"dark": "nord9",
"light": "nord9"
},
"syntaxFunction": {
"dark": "nord8",
"light": "nord8"
},
"syntaxVariable": {
"dark": "nord7",
"light": "nord7"
},
"syntaxString": {
"dark": "nord14",
"light": "nord14"
},
"syntaxNumber": {
"dark": "nord15",
"light": "nord15"
},
"syntaxType": {
"dark": "nord7",
"light": "nord7"
},
"syntaxOperator": {
"dark": "nord9",
"light": "nord9"
},
"syntaxPunctuation": {
"dark": "nord4",
"light": "nord0"
}
}
}
+1
View File
@@ -137,3 +137,4 @@
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
+22 -22
View File
@@ -40,7 +40,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.58",
"version": "1.0.62",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -67,7 +67,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.58",
"version": "1.0.62",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -91,7 +91,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.58",
"version": "1.0.62",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -115,7 +115,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.58",
"version": "1.0.62",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -155,7 +155,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.58",
"version": "1.0.62",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -171,7 +171,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.58",
"version": "1.0.62",
"bin": {
"opencode": "./bin/opencode",
},
@@ -189,8 +189,8 @@
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opentui/core": "0.0.0-20251108-0c7899b1",
"@opentui/solid": "0.0.0-20251108-0c7899b1",
"@opentui/core": "0.1.42",
"@opentui/solid": "0.1.42",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -249,7 +249,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.58",
"version": "1.0.62",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -269,7 +269,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.58",
"version": "1.0.62",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@@ -280,7 +280,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.58",
"version": "1.0.62",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -293,7 +293,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.58",
"version": "1.0.62",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -323,7 +323,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.58",
"version": "1.0.62",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -966,21 +966,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.0.0-20251108-0c7899b1", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251108-0c7899b1", "@opentui/core-darwin-x64": "0.0.0-20251108-0c7899b1", "@opentui/core-linux-arm64": "0.0.0-20251108-0c7899b1", "@opentui/core-linux-x64": "0.0.0-20251108-0c7899b1", "@opentui/core-win32-arm64": "0.0.0-20251108-0c7899b1", "@opentui/core-win32-x64": "0.0.0-20251108-0c7899b1", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-uJ7wbVw2v5NnL6g3v72SjPLUwMl2wqOejUEo8t4NeBA8nsboSxggqkrqOYf6OOmCADoAqyFDY7akZMsz6HMZtg=="],
"@opentui/core": ["@opentui/core@0.1.42", "", { "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.42", "@opentui/core-darwin-x64": "0.1.42", "@opentui/core-linux-arm64": "0.1.42", "@opentui/core-linux-x64": "0.1.42", "@opentui/core-win32-arm64": "0.1.42", "@opentui/core-win32-x64": "0.1.42", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-oV2xHBB2HaNiGvaV6R0C8GmniNJSsLKop4APq4FrLyCYberc6vZcATSHcA5YT9krdvHbBDOOn9RI2oaVJYRbUQ=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251108-0c7899b1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DS9CmFmZZjwe6PIhz6zhZAsDx11DtyMFDxn8V3On2b8G892aBG6rHYtBBnsM28/1GGEJBTeDQ/jUXPVd6FNJ/g=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.42", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sk5b/kh/y8HUJ7stGA5ydkajJX/z2OiGqSm+wn6XIoqdDavxQaFoQOt1PCuCqaxqZWJcXZ6OmISDVagZPUsPuw=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251108-0c7899b1", "", { "os": "darwin", "cpu": "x64" }, "sha512-K4XwdmT6FTShn7EG8AKliPzO5H59R0XUlZi9+kfRVW59IIJtna5wxbu69SkA28dFoWj5i4yDumwoBI+tI7T6vg=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.42", "", { "os": "darwin", "cpu": "x64" }, "sha512-b0FKTw+t/wlJg4u+wTurWzbQe47gExkjguaGSUua0m0vybrkkvbUvmrADr+yivCjxcPAhSZ3lOOVU3uZuWsNqw=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251108-0c7899b1", "", { "os": "linux", "cpu": "arm64" }, "sha512-3JUmxZeSvxV5yU7NEXSecy5Z1/LcVUMy1oWyusZgp96X0CTYAXMrolZt9IJDGO5raeO7JId1UaJmWW0r4DR8TA=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.42", "", { "os": "linux", "cpu": "arm64" }, "sha512-Vy8BrjJpv2f56JAsYmv4PkC+2HsCv8Gh0ErrlIJQ8L4h29oWabS44m0uxFdvjuTDgKpCJzOScsxsy1VGzSd9rw=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251108-0c7899b1", "", { "os": "linux", "cpu": "x64" }, "sha512-i/AQWGyanpPRpk9NK7Ze1tn+d5bqzM9wZFKNB3rd9d2Vbt/ROgBJItG6igz8vzKPKgnlHK4Gw9b5iG5sbjpd+Q=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.42", "", { "os": "linux", "cpu": "x64" }, "sha512-cO+13E1HIAPUdV/DRdKotHFAxsLc+ipbbFKGAuu/msfvywCnnNs86w22yeMg0cEqx7aBocWWT1XfJEHDJLFOqw=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251108-0c7899b1", "", { "os": "win32", "cpu": "arm64" }, "sha512-C7JLWuNN3w2txiVx3demwNwogVi4DQB5ZNHy2b09++kd2m449/RwGPyLcKpuoTzU4s/usYOeY4TxKIAd8cKedQ=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.42", "", { "os": "win32", "cpu": "arm64" }, "sha512-xpLhODjOWh7gMOSrKIldb4v6hR0TGyz6kjckDKwcjUv3LGbLJuSly+3O/zuWWS60dt56G1X4A0OyjWwiGZjc0g=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251108-0c7899b1", "", { "os": "win32", "cpu": "x64" }, "sha512-mpOryp37YaHlTsN70LhiSn9hJJBktbyhlH/eB3N2K7H1ANYQVrekgBJ3rDxlH1GDVtRz6vLS3IDlyK75qNX4pg=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.42", "", { "os": "win32", "cpu": "x64" }, "sha512-pao5XdAln93WWPdsTF+V+HccZ5d1ijSmv0OoBbkjkVbP+tiN41yxNqg/7jzW9IiAakYsvmpKV+3ixi/dlBEvOQ=="],
"@opentui/solid": ["@opentui/solid@0.0.0-20251108-0c7899b1", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251108-0c7899b1", "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-tcsYnFGH/KBlQNG0IyZE2bisnm5NwN/w7theuWga3L1zoXqZqA5dQHutAVg4zkq5l/YKULeDI4jBlvz0lzH88A=="],
"@opentui/solid": ["@opentui/solid@0.1.42", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.42", "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-4TNlEtatZ4n9TcKPWSF/EoaPaLmZuFVJ4hHh9wRggNaGrmDlmJ+9N/8oEKXETt+oRDX/1CdowAaTOVfaqb1t6g=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -1680,7 +1680,7 @@
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-ffi-structs": ["bun-ffi-structs@0.1.0", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-NoRfJ81pgLIHCzw624/2GS2FuxcU0G4SRJww/4PXvheNVUPSIUjkOC6v1/8rk66tJVCb9oR0D6rDNKK0qT5O2Q=="],
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
+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.58"
"version": "1.0.62"
},
"dependencies": {
"@ibm/plex": "6.4.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.58",
"version": "1.0.62",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.58",
"version": "1.0.62",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.58",
"version": "1.0.62",
"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.58",
"version": "1.0.62",
"description": "",
"type": "module",
"scripts": {
@@ -345,7 +345,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return (
<div class="relative size-full _max-h-[320px] flex flex-col gap-3">
<Show when={store.popoverIsOpen}>
<div class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-[252px] min-h-10 overflow-y-auto flex flex-col p-2 pb-0 rounded-2xl border border-border-base bg-surface-raised-stronger-non-alpha shadow-md">
<div
class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-[252px] min-h-10
overflow-auto no-scrollbar flex flex-col p-2 pb-0 rounded-2xl
border border-border-base bg-surface-raised-stronger-non-alpha shadow-md"
>
<Show when={flat().length > 0} fallback={<div class="text-text-weak px-2">No matching files</div>}>
<For each={flat()}>
{(i) => (
@@ -362,7 +366,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{getDirectory(i)}
</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
<Show when={!i.endsWith("/")}>
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
</Show>
</div>
</div>
<div class="flex items-center gap-x-1 text-text-muted/40 shrink-0"></div>
+28 -16
View File
@@ -44,7 +44,7 @@ import {
useDragDropContext,
} from "@thisbeyond/solid-dnd"
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
import type { JSX } from "solid-js"
import type { JSX, ParentProps } from "solid-js"
import { useSync } from "@/context/sync"
import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
import { Markdown } from "@opencode-ai/ui"
@@ -477,7 +477,7 @@ export default function Page() {
class="flex flex-col items-start self-stretch gap-8 pb-20"
>
{/* Title */}
<div class="flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10 pb-1">
<div class="flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-20 pb-1">
<div class="w-full text-14-medium text-text-strong">
<Show
when={titled()}
@@ -524,7 +524,7 @@ export default function Page() {
<For each={message.summary?.diffs ?? []}>
{(diff) => (
<Accordion.Item value={diff.file}>
<Accordion.Header>
<StickyAccordionHeader class="top-10 data-expanded:before:-top-10 ">
<Accordion.Trigger>
<div class="flex items-center justify-between w-full gap-5">
<div class="grow flex items-center gap-5 min-w-0">
@@ -549,8 +549,8 @@ export default function Page() {
</div>
</div>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="max-h-[360px] overflow-y-auto no-scrollbar">
</StickyAccordionHeader>
<Accordion.Content class="max-h-60 overflow-y-auto no-scrollbar">
<Diff
before={{
name: diff.file!,
@@ -682,7 +682,7 @@ export default function Page() {
<For each={session.diffs()}>
{(diff) => (
<Accordion.Item value={diff.file} defaultOpen>
<Accordion.Header>
<StickyAccordionHeader>
<Accordion.Trigger>
<div class="flex items-center justify-between w-full gap-5">
<div class="grow flex items-center gap-5 min-w-0">
@@ -702,7 +702,7 @@ export default function Page() {
</div>
</div>
</Accordion.Trigger>
</Accordion.Header>
</StickyAccordionHeader>
<Accordion.Content>
<Diff
before={{
@@ -725,22 +725,19 @@ export default function Page() {
</div>
</Tabs.Content>
<Show when={local.layout.review.state() === "tab" && session.diffs().length}>
<Tabs.Content value="review" class="select-text">
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden mt-8">
<div
classList={{
"relative px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0": true,
"relative px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0 overflow-hidden": true,
}}
>
<div class="h-8 w-full flex items-center justify-between shrink-0 self-stretch sticky top-0 bg-background-stronger z-100">
<div class="flex items-center gap-x-3"></div>
</div>
<div class="text-14-medium text-text-strong">All changes</div>
<div class="h-full pb-40 overflow-y-auto no-scrollbar">
<div class="text-14-medium text-text-strong shrink-0">All changes</div>
<div class="flex-1 min-h-0 pb-40 overflow-y-auto no-scrollbar">
<Accordion class="w-full" multiple>
<For each={session.diffs()}>
{(diff) => (
<Accordion.Item value={diff.file} defaultOpen>
<Accordion.Header>
<StickyAccordionHeader>
<Accordion.Trigger>
<div class="flex items-center justify-between w-full gap-5">
<div class="grow flex items-center gap-5 min-w-0">
@@ -758,7 +755,7 @@ export default function Page() {
</div>
</div>
</Accordion.Trigger>
</Accordion.Header>
</StickyAccordionHeader>
<Accordion.Content>
<Diff
diffStyle="split"
@@ -898,3 +895,18 @@ export default function Page() {
</div>
)
}
function StickyAccordionHeader(props: ParentProps<{ class?: string }>) {
return (
<Accordion.Header
classList={{
"sticky top-0 data-expanded:z-10": true,
"data-expanded:before:content-[''] data-expanded:before:z-[-10]": true,
"data-expanded:before:absolute data-expanded:before:inset-0 data-expanded:before:bg-background-stronger": true,
[props.class ?? ""]: !!props.class,
}}
>
{props.children}
</Accordion.Header>
)
}
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The AI coding agent built for the terminal"
version = "1.0.58"
version = "1.0.62"
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.58/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.62/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.58/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.62/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.58/opencode-linux-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.62/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.58/opencode-linux-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.62/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.58/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.62/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.58",
"version": "1.0.62",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.58",
"version": "1.0.62",
"name": "opencode",
"type": "module",
"private": true,
@@ -54,8 +54,8 @@
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opentui/core": "0.0.0-20251108-0c7899b1",
"@opentui/solid": "0.0.0-20251108-0c7899b1",
"@opentui/core": "0.1.42",
"@opentui/solid": "0.1.42",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
+3 -1
View File
@@ -16,6 +16,7 @@ export namespace Agent {
builtIn: z.boolean(),
topP: z.number().optional(),
temperature: z.number().optional(),
color: z.string().optional(),
permission: z.object({
edit: Config.Permission,
bash: z.record(z.string(), Config.Permission),
@@ -147,7 +148,7 @@ export namespace Agent {
tools: {},
builtIn: false,
}
const { name, model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
const { name, model, prompt, tools, description, temperature, top_p, mode, permission, color, ...extra } = value
item.options = {
...item.options,
...extra,
@@ -167,6 +168,7 @@ export namespace Agent {
if (temperature != undefined) item.temperature = temperature
if (top_p != undefined) item.topP = top_p
if (mode) item.mode = mode
if (color) item.color = color
// just here for consistency & to prevent it from being added as an option
if (name) item.name = name
+11 -5
View File
@@ -439,9 +439,10 @@ export const GithubRunCommand = cmd({
// Local PR
if (prData.headRepository.nameWithOwner === prData.baseRepository.nameWithOwner) {
await checkoutLocalBranch(prData)
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
const dataPrompt = buildPromptDataForPR(prData)
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
if (await branchIsDirty()) {
if (await branchIsDirty(head)) {
const summary = await summarize(response)
await pushToLocalBranch(summary)
}
@@ -451,9 +452,10 @@ export const GithubRunCommand = cmd({
// Fork PR
else {
await checkoutForkBranch(prData)
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
const dataPrompt = buildPromptDataForPR(prData)
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
if (await branchIsDirty()) {
if (await branchIsDirty(head)) {
const summary = await summarize(response)
await pushToForkBranch(summary, prData)
}
@@ -464,10 +466,11 @@ export const GithubRunCommand = cmd({
// Issue
else {
const branch = await checkoutNewBranch()
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
const issueData = await fetchIssue()
const dataPrompt = buildPromptDataForIssue(issueData)
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
if (await branchIsDirty()) {
if (await branchIsDirty(head)) {
const summary = await summarize(response)
await pushToNewBranch(summary, branch)
const pr = await createPR(
@@ -832,10 +835,13 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
await $`git push fork HEAD:${remoteBranch}`
}
async function branchIsDirty() {
async function branchIsDirty(originalHead: string) {
console.log("Checking if branch is dirty...")
const ret = await $`git status --porcelain`
return ret.stdout.toString().trim().length > 0
const status = ret.stdout.toString().trim()
if (status.length > 0) return true
const head = await $`git rev-parse HEAD`
return head.stdout.toString().trim() !== originalHead
}
async function assertPermissions() {
+41 -5
View File
@@ -1,21 +1,57 @@
import type { Argv } from "yargs"
import { Instance } from "../../project/instance"
import { Provider } from "../../provider/provider"
import { cmd } from "./cmd"
import { UI } from "../ui"
import { EOL } from "os"
export const ModelsCommand = cmd({
command: "models",
command: "models [provider]",
describe: "list all available models",
handler: async () => {
builder: (yargs: Argv) => {
return yargs
.positional("provider", {
describe: "provider ID to filter models by",
type: "string",
array: false,
})
.option("verbose", {
describe: "use more verbose model output (includes metadata like costs)",
type: "boolean",
})
},
handler: async (args) => {
await Instance.provide({
directory: process.cwd(),
async fn() {
const providers = await Provider.list()
for (const [providerID, provider] of Object.entries(providers)) {
for (const modelID of Object.keys(provider.info.models)) {
console.log(`${providerID}/${modelID}`)
function printModels(providerID: string, verbose?: boolean) {
const provider = providers[providerID]
for (const [modelID, model] of Object.entries(provider.info.models)) {
process.stdout.write(`${providerID}/${modelID}`)
process.stdout.write(EOL)
if (verbose) {
process.stdout.write(JSON.stringify(model, null, 2))
process.stdout.write(EOL)
}
}
}
if (args.provider) {
const provider = providers[args.provider]
if (!provider) {
UI.error(`Provider not found: ${args.provider}`)
return
}
printModels(args.provider, args.verbose)
return
}
for (const providerID of Object.keys(providers)) {
printModels(providerID, args.verbose)
}
},
})
},
+112
View File
@@ -0,0 +1,112 @@
import { UI } from "../ui"
import { cmd } from "./cmd"
import { Instance } from "@/project/instance"
import { $ } from "bun"
export const PrCommand = cmd({
command: "pr <number>",
describe: "fetch and checkout a GitHub PR branch, then run opencode",
builder: (yargs) =>
yargs.positional("number", {
type: "number",
describe: "PR number to checkout",
demandOption: true,
}),
async handler(args) {
await Instance.provide({
directory: process.cwd(),
async fn() {
const project = Instance.project
if (project.vcs !== "git") {
UI.error("Could not find git repository. Please run this command from a git repository.")
process.exit(1)
}
const prNumber = args.number
const localBranchName = `pr/${prNumber}`
UI.println(`Fetching and checking out PR #${prNumber}...`)
// Use gh pr checkout with custom branch name
const result = await $`gh pr checkout ${prNumber} --branch ${localBranchName} --force`.nothrow()
if (result.exitCode !== 0) {
UI.error(`Failed to checkout PR #${prNumber}. Make sure you have gh CLI installed and authenticated.`)
process.exit(1)
}
// Fetch PR info for fork handling and session link detection
const prInfoResult =
await $`gh pr view ${prNumber} --json headRepository,headRepositoryOwner,isCrossRepository,headRefName,body`.nothrow()
let sessionId: string | undefined
if (prInfoResult.exitCode === 0) {
const prInfoText = prInfoResult.text()
if (prInfoText.trim()) {
const prInfo = JSON.parse(prInfoText)
// Handle fork PRs
if (prInfo && prInfo.isCrossRepository && prInfo.headRepository && prInfo.headRepositoryOwner) {
const forkOwner = prInfo.headRepositoryOwner.login
const forkName = prInfo.headRepository.name
const remoteName = forkOwner
// Check if remote already exists
const remotes = (await $`git remote`.nothrow().text()).trim()
if (!remotes.split("\n").includes(remoteName)) {
await $`git remote add ${remoteName} https://github.com/${forkOwner}/${forkName}.git`.nothrow()
UI.println(`Added fork remote: ${remoteName}`)
}
// Set upstream to the fork so pushes go there
const headRefName = prInfo.headRefName
await $`git branch --set-upstream-to=${remoteName}/${headRefName} ${localBranchName}`.nothrow()
}
// Check for opencode session link in PR body
if (prInfo && prInfo.body) {
const sessionMatch = prInfo.body.match(/https:\/\/opencode\.ai\/s\/([a-zA-Z0-9_-]+)/)
if (sessionMatch) {
const sessionUrl = sessionMatch[0]
UI.println(`Found opencode session: ${sessionUrl}`)
UI.println(`Importing session...`)
const importResult = await $`opencode import ${sessionUrl}`.nothrow()
if (importResult.exitCode === 0) {
const importOutput = importResult.text().trim()
// Extract session ID from the output (format: "Imported session: <session-id>")
const sessionIdMatch = importOutput.match(/Imported session: ([a-zA-Z0-9_-]+)/)
if (sessionIdMatch) {
sessionId = sessionIdMatch[1]
UI.println(`Session imported: ${sessionId}`)
}
}
}
}
}
}
UI.println(`Successfully checked out PR #${prNumber} as branch '${localBranchName}'`)
UI.println()
UI.println("Starting opencode...")
UI.println()
// Launch opencode TUI with session ID if available
const { spawn } = await import("child_process")
const opencodeArgs = sessionId ? ["-s", sessionId] : []
const opencodeProcess = spawn("opencode", opencodeArgs, {
stdio: "inherit",
cwd: process.cwd(),
})
await new Promise<void>((resolve, reject) => {
opencodeProcess.on("exit", (code) => {
if (code === 0) resolve()
else reject(new Error(`opencode exited with code ${code}`))
})
opencodeProcess.on("error", reject)
})
},
})
},
})
@@ -488,6 +488,8 @@ function ErrorComponent(props: { error: Error; reset: () => void; onExit: () =>
)
}
issueURL.searchParams.set("opencode-version", Installation.VERSION)
const copyIssueURL = () => {
Clipboard.copy(issueURL.toString()).then(() => {
setCopied(true)
@@ -8,6 +8,7 @@ import { useSync } from "@tui/context/sync"
import { useTheme } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border"
import { useCommandDialog } from "@tui/component/dialog-command"
import { Locale } from "@/util/locale"
import type { PromptInfo } from "./history"
export type AutocompleteRef = {
@@ -125,10 +126,11 @@ export function Autocomplete(props: {
// Add file options
if (!result.error && result.data) {
const width = store.position.width - 4
options.push(
...result.data.map(
(item): AutocompleteOption => ({
display: item,
display: Locale.truncateMiddle(item, width),
onSelect: () => {
insertPart(item, {
type: "file",
@@ -217,12 +219,6 @@ export function Autocomplete(props: {
description: "compact the session",
onSelect: () => command.trigger("session.compact"),
},
{
display: "/share",
disabled: !!s.share?.url,
description: "share a session",
onSelect: () => command.trigger("session.share"),
},
{
display: "/unshare",
disabled: !s.share,
@@ -250,7 +246,16 @@ export function Autocomplete(props: {
onSelect: () => command.trigger("session.timeline"),
},
)
if (sync.data.config.share !== "disabled") {
results.push({
display: "/share",
disabled: !!s.share?.url,
description: "share a session",
onSelect: () => command.trigger("session.share"),
})
}
}
results.push(
{
display: "/new",
@@ -101,16 +101,81 @@ export function Prompt(props: PromptProps) {
value: "prompt.editor",
onSelect: async (dialog, trigger) => {
dialog.clear()
const value = trigger === "prompt" ? "" : input.plainText
// replace summarized text parts with the actual text
const text = store.prompt.parts
.filter((p) => p.type === "text")
.reduce((acc, p) => {
if (!p.source) return acc
return acc.replace(p.source.text.value, p.text)
}, store.prompt.input)
const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text")
const value = trigger === "prompt" ? "" : text
const content = await Editor.open({ value, renderer })
if (content) {
input.setText(content, { history: false })
setStore("prompt", {
input: content,
parts: [],
if (!content) return
input.setText(content, { history: false })
// Update positions for nonTextParts based on their location in new content
// Filter out parts whose virtual text was deleted
// this handles a case where the user edits the text in the editor
// such that the virtual text moves around or is deleted
const updatedNonTextParts = nonTextParts
.map((part) => {
let virtualText = ""
if (part.type === "file" && part.source?.text) {
virtualText = part.source.text.value
} else if (part.type === "agent" && part.source) {
virtualText = part.source.value
}
if (!virtualText) return part
const newStart = content.indexOf(virtualText)
// if the virtual text is deleted, remove the part
if (newStart === -1) return null
const newEnd = newStart + virtualText.length
if (part.type === "file" && part.source?.text) {
return {
...part,
source: {
...part.source,
text: {
...part.source.text,
start: newStart,
end: newEnd,
},
},
}
}
if (part.type === "agent" && part.source) {
return {
...part,
source: {
...part.source,
start: newStart,
end: newEnd,
},
}
}
return part
})
input.cursorOffset = Bun.stringWidth(content)
}
.filter((part) => part !== null)
setStore("prompt", {
input: content,
// keep only the non-text parts because the text parts were
// already expanded inline
parts: updatedNonTextParts,
})
restoreExtmarksFromParts(updatedNonTextParts)
input.cursorOffset = Bun.stringWidth(content)
},
},
{
@@ -163,11 +228,21 @@ export function Prompt(props: PromptProps) {
if (!props.sessionID) return
if (autocomplete.visible) return
if (!input.focused) return
sdk.client.session.abort({
path: {
id: props.sessionID,
},
})
setStore("interrupt", store.interrupt + 1)
setTimeout(() => {
setStore("interrupt", 0)
}, 5000)
if (store.interrupt >= 2) {
sdk.client.session.abort({
path: {
id: props.sessionID,
},
})
setStore("interrupt", 0)
}
dialog.clear()
},
},
@@ -187,6 +262,7 @@ export function Prompt(props: PromptProps) {
prompt: PromptInfo
mode: "normal" | "shell"
extmarkToPartIndex: Map<number, number>
interrupt: number
}>({
prompt: {
input: "",
@@ -194,6 +270,7 @@ export function Prompt(props: PromptProps) {
},
mode: "normal",
extmarkToPartIndex: new Map(),
interrupt: 0,
})
createEffect(() => {
@@ -681,8 +758,11 @@ export function Prompt(props: PromptProps) {
</Match>
<Match when={status() === "working"}>
<box flexDirection="row" gap={1}>
<text fg={theme.text}>
esc <span style={{ fg: theme.textMuted }}>interrupt</span>
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
esc{" "}
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
</span>
</text>
</box>
</Match>
@@ -90,6 +90,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
},
color(name: string) {
const agent = agents().find((x) => x.name === name)
if (agent?.color) return agent.color
const index = agents().findIndex((x) => x.name === name)
return colors()[index % colors().length]
},
@@ -217,9 +217,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
// blocking
Promise.all([
sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
sdk.client.config.get().then((x) => setStore("config", x.data!)),
sdk.client.config.providers({ throwOnError: true }).then((x) => setStore("provider", x.data!.providers)),
sdk.client.app.agents({ throwOnError: true }).then((x) => setStore("agent", x.data ?? [])),
sdk.client.config.get({ throwOnError: true }).then((x) => setStore("config", x.data!)),
]).then(() => {
setStore("status", "partial")
// non-blocking
@@ -1,5 +1,6 @@
import { SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
import { createMemo } from "solid-js"
import path from "path"
import { createEffect, createMemo, onMount } from "solid-js"
import { useSync } from "@tui/context/sync"
import { createSimpleContext } from "./helper"
import aura from "./theme/aura.json" with { type: "json" }
@@ -27,7 +28,9 @@ import vesper from "./theme/vesper.json" with { type: "json" }
import zenburn from "./theme/zenburn.json" with { type: "json" }
import { useKV } from "./kv"
import { useRenderer } from "@opentui/solid"
import { createStore } from "solid-js/store"
import { createStore, produce } from "solid-js/store"
import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem"
type Theme = {
primary: RGBA
@@ -144,6 +147,17 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
themes: DEFAULT_THEMES,
mode: props.mode,
active: (sync.data.config.theme ?? kv.get("theme", "opencode")) as string,
ready: false,
})
createEffect(async () => {
const custom = await getCustomThemes()
setStore(
produce((draft) => {
Object.assign(draft.themes, custom)
draft.ready = true
}),
)
})
const renderer = useRenderer()
@@ -187,12 +201,39 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
kv.set("theme", theme)
},
get ready() {
return sync.ready
return store.ready
},
}
},
})
const CUSTOM_THEME_GLOB = new Bun.Glob("themes/*.json")
async function getCustomThemes() {
const directories = [
Global.Path.config,
...(await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
start: process.cwd(),
}),
)),
]
const result: Record<string, ThemeJson> = {}
for (const dir of directories) {
for await (const item of CUSTOM_THEME_GLOB.scan({
absolute: true,
followSymlinks: true,
dot: true,
cwd: dir,
})) {
const name = path.basename(item, ".json")
result[name] = await Bun.file(item).json()
}
}
return result
}
function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson {
const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
@@ -3,8 +3,13 @@ import { useSync } from "@tui/context/sync"
import { DialogSelect } from "@tui/ui/dialog-select"
import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route"
import type { PromptInfo } from "@tui/component/prompt/history"
export function DialogMessage(props: { messageID: string; sessionID: string }) {
export function DialogMessage(props: {
messageID: string
sessionID: string
setPrompt?: (prompt: PromptInfo) => void
}) {
const sync = useSync()
const sdk = useSDK()
const message = createMemo(() => sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID))
@@ -19,14 +24,33 @@ export function DialogMessage(props: { messageID: string; sessionID: string }) {
value: "session.revert",
description: "undo messages and file changes",
onSelect: (dialog) => {
const msg = message()
if (!msg) return
sdk.client.session.revert({
path: {
id: props.sessionID,
},
body: {
messageID: message()!.id,
messageID: msg.id,
},
})
if (props.setPrompt) {
const parts = sync.data.part[msg.id]
const promptInfo = parts.reduce(
(agg, part) => {
if (part.type === "text") {
if (!part.synthetic) agg.input += part.text
}
if (part.type === "file") agg.parts.push(part)
return agg
},
{ input: "", parts: [] as PromptInfo["parts"] },
)
props.setPrompt(promptInfo)
}
dialog.clear()
},
},
@@ -217,29 +217,33 @@ export function Session() {
dialog.clear()
},
},
{
title: "Share session",
value: "session.share",
keybind: "session_share",
disabled: !!session()?.share?.url,
category: "Session",
onSelect: async (dialog) => {
await sdk.client.session
.share({
path: {
id: route.sessionID,
...(sync.data.config.share !== "disabled"
? [
{
title: "Share session",
value: "session.share",
keybind: "session_share" as const,
disabled: !!session()?.share?.url,
category: "Session",
onSelect: async (dialog: any) => {
await sdk.client.session
.share({
path: {
id: route.sessionID,
},
})
.then((res) =>
Clipboard.copy(res.data!.share!.url).catch(() =>
toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }),
),
)
.then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
.catch(() => toast.show({ message: "Failed to share session", variant: "error" }))
dialog.clear()
},
})
.then((res) =>
Clipboard.copy(res.data!.share!.url).catch(() =>
toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }),
),
)
.then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
.catch(() => toast.show({ message: "Failed to share session", variant: "error" }))
dialog.clear()
},
},
},
]
: []),
{
title: "Unshare session",
value: "session.unshare",
@@ -753,7 +757,13 @@ export function Session() {
index={index()}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
dialog.replace(() => <DialogMessage messageID={message.id} sessionID={route.sessionID} />)
dialog.replace(() => (
<DialogMessage
messageID={message.id}
sessionID={route.sessionID}
setPrompt={(promptInfo) => prompt.set(promptInfo)}
/>
))
}}
message={message as UserMessage}
parts={sync.data.part[message.id] ?? []}
@@ -1,5 +1,5 @@
import { useSync } from "@tui/context/sync"
import { createMemo, For, Show, Switch, Match } from "solid-js"
import { createMemo, For, Show, Switch, Match, createSignal } from "solid-js"
import { useTheme } from "../../context/theme"
import { Locale } from "@/util/locale"
import path from "path"
@@ -13,6 +13,11 @@ export function Sidebar(props: { sessionID: string }) {
const todo = createMemo(() => sync.data.todo[props.sessionID] ?? [])
const messages = createMemo(() => sync.data.message[props.sessionID] ?? [])
const [mcpExpanded, setMcpExpanded] = createSignal(true)
const [diffExpanded, setDiffExpanded] = createSignal(true)
const [todoExpanded, setTodoExpanded] = createSignal(true)
const [lspExpanded, setLspExpanded] = createSignal(true)
const cost = createMemo(() => {
const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
return new Intl.NumberFormat("en-US", {
@@ -55,110 +60,130 @@ export function Sidebar(props: { sessionID: string }) {
</box>
<Show when={Object.keys(sync.data.mcp).length > 0}>
<box>
<text fg={theme.text}>
<b>MCP</b>
</text>
<For each={Object.entries(sync.data.mcp)}>
{([key, item]) => (
<box flexDirection="row" gap={1}>
<text
flexShrink={0}
style={{
fg: {
connected: theme.success,
failed: theme.error,
disabled: theme.textMuted,
}[item.status],
}}
>
</text>
<text fg={theme.text} wrapMode="word">
{key}{" "}
<span style={{ fg: theme.textMuted }}>
<Switch>
<Match when={item.status === "connected"}>Connected</Match>
<Match when={item.status === "failed" && item}>{(val) => <i>{val().error}</i>}</Match>
<Match when={item.status === "disabled"}>Disabled in configuration</Match>
</Switch>
</span>
</text>
</box>
)}
</For>
<box flexDirection="row" gap={1} onMouseDown={() => setMcpExpanded(!mcpExpanded())}>
<text fg={theme.text}>{mcpExpanded() ? "▼" : "▶"}</text>
<text fg={theme.text}>
<b>MCP</b>
</text>
</box>
<Show when={mcpExpanded()}>
<For each={Object.entries(sync.data.mcp)}>
{([key, item]) => (
<box flexDirection="row" gap={1}>
<text
flexShrink={0}
style={{
fg: {
connected: theme.success,
failed: theme.error,
disabled: theme.textMuted,
}[item.status],
}}
>
</text>
<text fg={theme.text} wrapMode="word">
{key}{" "}
<span style={{ fg: theme.textMuted }}>
<Switch>
<Match when={item.status === "connected"}>Connected</Match>
<Match when={item.status === "failed" && item}>{(val) => <i>{val().error}</i>}</Match>
<Match when={item.status === "disabled"}>Disabled in configuration</Match>
</Switch>
</span>
</text>
</box>
)}
</For>
</Show>
</box>
</Show>
<Show when={sync.data.lsp.length > 0}>
<box>
<text fg={theme.text}>
<b>LSP</b>
</text>
<For each={sync.data.lsp}>
{(item) => (
<box flexDirection="row" gap={1}>
<text
flexShrink={0}
style={{
fg: {
connected: theme.success,
error: theme.error,
}[item.status],
}}
>
</text>
<text fg={theme.textMuted}>
{item.id} {item.root}
</text>
</box>
)}
</For>
</box>
</Show>
<Show when={diff().length > 0}>
<box>
<text fg={theme.text}>
<b>Modified Files</b>
</text>
<For each={diff() || []}>
{(item) => {
const file = createMemo(() => {
const splits = item.file.split(path.sep).filter(Boolean)
const last = splits.at(-1)!
const rest = splits.slice(0, -1).join(path.sep)
return Locale.truncateMiddle(rest, 30 - last.length) + "/" + last
})
return (
<box flexDirection="row" gap={1} justifyContent="space-between">
<text fg={theme.textMuted} wrapMode="char">
{file()}
<box flexDirection="row" gap={1} onMouseDown={() => setLspExpanded(!lspExpanded())}>
<text fg={theme.text}>{lspExpanded() ? "▼" : "▶"}</text>
<text fg={theme.text}>
<b>LSP</b>
</text>
</box>
<Show when={lspExpanded()}>
<For each={sync.data.lsp}>
{(item) => (
<box flexDirection="row" gap={1}>
<text
flexShrink={0}
style={{
fg: {
connected: theme.success,
error: theme.error,
}[item.status],
}}
>
</text>
<text fg={theme.textMuted}>
{item.id} {item.root}
</text>
<box flexDirection="row" gap={1} flexShrink={0}>
<Show when={item.additions}>
<text fg={theme.diffAdded}>+{item.additions}</text>
</Show>
<Show when={item.deletions}>
<text fg={theme.diffRemoved}>-{item.deletions}</text>
</Show>
</box>
</box>
)
}}
</For>
)}
</For>
</Show>
</box>
</Show>
<Show when={todo().length > 0}>
<box>
<text fg={theme.text}>
<b>Todo</b>
</text>
<For each={todo()}>
{(todo) => (
<text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
[{todo.status === "completed" ? "✓" : " "}] {todo.content}
</text>
)}
</For>
<box flexDirection="row" gap={1} onMouseDown={() => setTodoExpanded(!todoExpanded())}>
<text fg={theme.text}>{todoExpanded() ? "▼" : "▶"}</text>
<text fg={theme.text}>
<b>Todo</b>
</text>
</box>
<Show when={todoExpanded()}>
<For each={todo()}>
{(todo) => (
<text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
[{todo.status === "completed" ? "✓" : " "}] {todo.content}
</text>
)}
</For>
</Show>
</box>
</Show>
<Show when={diff().length > 0}>
<box>
<box flexDirection="row" gap={1} onMouseDown={() => setDiffExpanded(!diffExpanded())}>
<text fg={theme.text}>{diffExpanded() ? "▼" : "▶"}</text>
<text fg={theme.text}>
<b>Modified Files</b>
</text>
</box>
<Show when={diffExpanded()}>
<For each={diff() || []}>
{(item) => {
const file = createMemo(() => {
const splits = item.file.split(path.sep).filter(Boolean)
const last = splits.at(-1)!
const rest = splits.slice(0, -1).join(path.sep)
return Locale.truncateMiddle(rest, 30 - last.length) + "/" + last
})
return (
<box flexDirection="row" gap={1} justifyContent="space-between">
<text fg={theme.textMuted} wrapMode="char">
{file()}
</text>
<box flexDirection="row" gap={1} flexShrink={0}>
<Show when={item.additions}>
<text fg={theme.diffAdded}>+{item.additions}</text>
</Show>
<Show when={item.deletions}>
<text fg={theme.diffRemoved}>-{item.deletions}</text>
</Show>
</box>
</box>
)
}}
</For>
</Show>
</box>
</Show>
</box>
@@ -1,5 +1,5 @@
import { $ } from "bun"
import { platform } from "os"
import { platform, release } from "os"
import clipboardy from "clipboardy"
import { lazy } from "../../../../util/lazy.js"
import { tmpdir } from "os"
@@ -29,6 +29,18 @@ export namespace Clipboard {
}
}
if (os === "win32" || release().includes("WSL")) {
const script =
"Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }"
const base64 = await $`powershell.exe -command "${script}"`.nothrow().text()
if (base64) {
const imageBuffer = Buffer.from(base64.trim(), "base64")
if (imageBuffer.length > 0) {
return { data: imageBuffer.toString("base64"), mime: "image/png" }
}
}
}
if (os === "linux") {
const wayland = await $`wl-paste -t image/png`.nothrow().arrayBuffer()
if (wayland && wayland.byteLength > 0) {
@@ -40,18 +52,6 @@ export namespace Clipboard {
}
}
if (os === "win32") {
const script =
"Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }"
const base64 = await $`powershell -command "${script}"`.nothrow().text()
if (base64) {
const imageBuffer = Buffer.from(base64.trim(), "base64")
if (imageBuffer.length > 0) {
return { data: imageBuffer.toString("base64"), mime: "image/png" }
}
}
}
const text = await clipboardy.read().catch(() => {})
if (text) {
return { data: text, mime: "text/plain" }
+21 -12
View File
@@ -24,12 +24,6 @@ export namespace Config {
export const state = Instance.state(async () => {
const auth = await Auth.all()
let result = await global()
for (const file of ["opencode.jsonc", "opencode.json"]) {
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
for (const resolved of found.toReversed()) {
result = mergeDeep(result, await loadFile(resolved))
}
}
// Override with custom config if provided
if (Flag.OPENCODE_CONFIG) {
@@ -37,6 +31,13 @@ export namespace Config {
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
}
for (const file of ["opencode.jsonc", "opencode.json"]) {
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
for (const resolved of found.toReversed()) {
result = mergeDeep(result, await loadFile(resolved))
}
}
if (Flag.OPENCODE_CONFIG_CONTENT) {
result = mergeDeep(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
@@ -74,12 +75,15 @@ export namespace Config {
for (const dir of directories) {
await assertValid(dir)
for (const file of ["opencode.jsonc", "opencode.json"]) {
result = mergeDeep(result, await loadFile(path.join(dir, file)))
// to satisy the type checker
result.agent ??= {}
result.mode ??= {}
result.plugin ??= []
if (dir.endsWith(".opencode")) {
for (const file of ["opencode.jsonc", "opencode.json"]) {
log.debug(`loading config from ${path.join(dir, file)}`)
result = mergeDeep(result, await loadFile(path.join(dir, file)))
// to satisy the type checker
result.agent ??= {}
result.mode ??= {}
result.plugin ??= []
}
}
promises.push(installDependencies(dir))
@@ -355,6 +359,11 @@ export namespace Config {
disable: z.boolean().optional(),
description: z.string().optional().describe("Description of when to use the agent"),
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
color: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format")
.optional()
.describe("Hex color code for the agent (e.g., #FF5733)"),
permission: z
.object({
edit: Permission.optional(),
+2
View File
@@ -24,6 +24,7 @@ import { TuiSpawnCommand } from "./cli/cmd/tui/spawn"
import { AcpCommand } from "./cli/cmd/acp"
import { EOL } from "os"
import { WebCommand } from "./cli/cmd/web"
import { PrCommand } from "./cli/cmd/pr"
process.on("unhandledRejection", (e) => {
Log.Default.error("rejection", {
@@ -90,6 +91,7 @@ const cli = yargs(hideBin(process.argv))
.command(ExportCommand)
.command(ImportCommand)
.command(GithubCommand)
.command(PrCommand)
.fail((msg) => {
if (
msg.startsWith("Unknown argument") ||
+64 -27
View File
@@ -103,6 +103,7 @@ export namespace LSP {
broken: new Set<string>(),
servers,
clients,
spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
}
},
async (state) => {
@@ -145,6 +146,49 @@ export namespace LSP {
const s = await state()
const extension = path.parse(file).ext || file
const result: LSPClient.Info[] = []
async function schedule(server: LSPServer.Info, root: string, key: string) {
const handle = await server
.spawn(root)
.then((value) => {
if (!value) s.broken.add(key)
return value
})
.catch((err) => {
s.broken.add(key)
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
return undefined
})
if (!handle) return undefined
log.info("spawned lsp server", { serverID: server.id })
const client = await LSPClient.create({
serverID: server.id,
server: handle,
root,
}).catch((err) => {
s.broken.add(key)
handle.process.kill()
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
return undefined
})
if (!client) {
handle.process.kill()
return undefined
}
const existing = s.clients.find((x) => x.root === root && x.serverID === server.id)
if (existing) {
handle.process.kill()
return existing
}
s.clients.push(client)
return client
}
for (const server of Object.values(s.servers)) {
if (server.extensions.length && !server.extensions.includes(extension)) continue
const root = await server.root(file)
@@ -156,39 +200,31 @@ export namespace LSP {
result.push(match)
continue
}
const handle = await server
.spawn(root)
.then((h) => {
if (h === undefined) {
s.broken.add(root + server.id)
}
return h
})
.catch((err) => {
s.broken.add(root + server.id)
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
return undefined
})
if (!handle) continue
log.info("spawned lsp server", { serverID: server.id })
const client = await LSPClient.create({
serverID: server.id,
server: handle,
root,
}).catch((err) => {
s.broken.add(root + server.id)
handle.process.kill()
log.error(`Failed to initialize LSP client ${server.id}`, {
error: err,
})
return undefined
const inflight = s.spawning.get(root + server.id)
if (inflight) {
const client = await inflight
if (!client) continue
result.push(client)
continue
}
const task = schedule(server, root, root + server.id)
s.spawning.set(root + server.id, task)
task.finally(() => {
if (s.spawning.get(root + server.id) === task) {
s.spawning.delete(root + server.id)
}
})
const client = await task
if (!client) continue
s.clients.push(client)
result.push(client)
Bus.publish(Event.Updated, {})
}
return result
}
@@ -199,6 +235,7 @@ export namespace LSP {
if (!clients.includes(client)) return
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
await client.notify.open({ path: input })
return wait
}).catch((err) => {
log.error("failed to touch file", { err, file: input })
+159 -63
View File
@@ -547,6 +547,40 @@ export namespace LSPServer {
},
}
export const SourceKit: Info = {
id: "sourcekit-lsp",
extensions: [".swift", ".objc", "objcpp"],
root: NearestRoot(["Package.swift", "*.xcodeproj", "*.xcworkspace"]),
async spawn(root) {
// Check if sourcekit-lsp is available in the PATH
// This is installed with the Swift toolchain
const sourcekit = Bun.which("sourcekit-lsp")
if (sourcekit) {
return {
process: spawn(sourcekit, {
cwd: root,
}),
}
}
// If sourcekit-lsp not found, check if xcrun is available
// This is specific to macOS where sourcekit-lsp is typically installed with Xcode
if (!Bun.which("xcrun")) return
const lspLoc = await $`xcrun --find sourcekit-lsp`.quiet().nothrow()
if (lspLoc.exitCode !== 0) return
const bin = lspLoc.text().trim()
return {
process: spawn(bin, {
cwd: root,
}),
}
},
}
export const RustAnalyzer: Info = {
id: "rust",
root: async (root) => {
@@ -598,73 +632,135 @@ export namespace LSPServer {
root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
async spawn(root) {
let bin = Bun.which("clangd", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
if (!bin) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading clangd from GitHub releases")
const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
if (!releaseResponse.ok) {
log.error("Failed to fetch clangd release info")
return
const args = ["--background-index", "--clang-tidy"]
const fromPath = Bun.which("clangd")
if (fromPath) {
return {
process: spawn(fromPath, args, {
cwd: root,
}),
}
const release = (await releaseResponse.json()) as any
const platform = process.platform
let assetName = ""
if (platform === "darwin") {
assetName = "clangd-mac-"
} else if (platform === "linux") {
assetName = "clangd-linux-"
} else if (platform === "win32") {
assetName = "clangd-windows-"
} else {
log.error(`Platform ${platform} is not supported by clangd auto-download`)
return
}
assetName += release.tag_name + ".zip"
const asset = release.assets.find((a: any) => a.name === assetName)
if (!asset) {
log.error(`Could not find asset ${assetName} in latest clangd release`)
return
}
const downloadUrl = asset.browser_download_url
const downloadResponse = await fetch(downloadUrl)
if (!downloadResponse.ok) {
log.error("Failed to download clangd")
return
}
const zipPath = path.join(Global.Path.bin, "clangd.zip")
await Bun.file(zipPath).write(downloadResponse)
await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
await fs.rm(zipPath, { force: true })
const extractedDir = path.join(Global.Path.bin, assetName.replace(".zip", ""))
bin = path.join(extractedDir, "bin", "clangd" + (platform === "win32" ? ".exe" : ""))
if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract clangd binary")
return
}
if (platform !== "win32") {
await $`chmod +x ${bin}`.nothrow()
}
log.info(`installed clangd`, { bin })
}
const ext = process.platform === "win32" ? ".exe" : ""
const direct = path.join(Global.Path.bin, "clangd" + ext)
if (await Bun.file(direct).exists()) {
return {
process: spawn(direct, args, {
cwd: root,
}),
}
}
const entries = await fs.readdir(Global.Path.bin, { withFileTypes: true }).catch(() => [])
for (const entry of entries) {
if (!entry.isDirectory()) continue
if (!entry.name.startsWith("clangd_")) continue
const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext)
if (await Bun.file(candidate).exists()) {
return {
process: spawn(candidate, args, {
cwd: root,
}),
}
}
}
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading clangd from GitHub releases")
const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
if (!releaseResponse.ok) {
log.error("Failed to fetch clangd release info")
return
}
const release: {
tag_name?: string
assets?: { name?: string; browser_download_url?: string }[]
} = await releaseResponse.json()
const tag = release.tag_name
if (!tag) {
log.error("clangd release did not include a tag name")
return
}
const platform = process.platform
const tokens: Record<string, string> = {
darwin: "mac",
linux: "linux",
win32: "windows",
}
const token = tokens[platform]
if (!token) {
log.error(`Platform ${platform} is not supported by clangd auto-download`)
return
}
const assets = release.assets ?? []
const valid = (item: { name?: string; browser_download_url?: string }) => {
if (!item.name) return false
if (!item.browser_download_url) return false
if (!item.name.includes(token)) return false
return item.name.includes(tag)
}
const asset =
assets.find((item) => valid(item) && item.name?.endsWith(".zip")) ??
assets.find((item) => valid(item) && item.name?.endsWith(".tar.xz")) ??
assets.find((item) => valid(item))
if (!asset?.name || !asset.browser_download_url) {
log.error("clangd could not match release asset", { tag, platform })
return
}
const name = asset.name
const downloadResponse = await fetch(asset.browser_download_url)
if (!downloadResponse.ok) {
log.error("Failed to download clangd")
return
}
const archive = path.join(Global.Path.bin, name)
const buf = await downloadResponse.arrayBuffer()
if (buf.byteLength === 0) {
log.error("Failed to write clangd archive")
return
}
await Bun.write(archive, buf)
const zip = name.endsWith(".zip")
const tar = name.endsWith(".tar.xz")
if (!zip && !tar) {
log.error("clangd encountered unsupported asset", { asset: name })
return
}
if (zip) {
await $`unzip -o -q ${archive}`.quiet().cwd(Global.Path.bin).nothrow()
}
if (tar) {
await $`tar -xf ${archive}`.cwd(Global.Path.bin).nothrow()
}
await fs.rm(archive, { force: true })
const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract clangd binary")
return
}
if (platform !== "win32") {
await $`chmod +x ${bin}`.nothrow()
}
await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {})
await fs.symlink(bin, path.join(Global.Path.bin, "clangd")).catch(() => {})
log.info(`installed clangd`, { bin })
return {
process: spawn(bin, ["--background-index", "--clang-tidy"], {
process: spawn(bin, args, {
cwd: root,
}),
}
+8 -2
View File
@@ -12,6 +12,7 @@ import { Auth } from "../auth"
import { Instance } from "../project/instance"
import { Global } from "../global"
import { Flag } from "../flag/flag"
import { iife } from "@/util/iife"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -289,10 +290,15 @@ export namespace Provider {
}
for (const [modelID, model] of Object.entries(provider.models ?? {})) {
const existing = parsed.models[modelID]
const existing = parsed.models[model.id ?? modelID]
const name = iife(() => {
if (model.name) return model.name
if (model.id && model.id !== modelID) return modelID
return existing?.name ?? modelID
})
const parsedModel: ModelsDev.Model = {
id: modelID,
name: model.name ?? existing?.name ?? modelID,
name,
release_date: model.release_date ?? existing?.release_date,
attachment: model.attachment ?? existing?.attachment ?? false,
reasoning: model.reasoning ?? existing?.reasoning ?? false,
+1
View File
@@ -163,6 +163,7 @@ export namespace Server {
return c.json(await Config.get())
},
)
.patch(
"/config",
describeRoute({
+11 -2
View File
@@ -378,8 +378,14 @@ export namespace Session {
metadata: z.custom<ProviderMetadata>().optional(),
}),
(input) => {
const cachedInputTokens = input.usage.cachedInputTokens ?? 0
const excludesCachedTokens = !!(input.metadata?.["anthropic"] || input.metadata?.["bedrock"])
const adjustedInputTokens = excludesCachedTokens
? (input.usage.inputTokens ?? 0)
: (input.usage.inputTokens ?? 0) - cachedInputTokens
const tokens = {
input: input.usage.inputTokens ?? 0,
input: adjustedInputTokens,
output: input.usage.outputTokens ?? 0,
reasoning: input.usage?.reasoningTokens ?? 0,
cache: {
@@ -387,7 +393,7 @@ export namespace Session {
// @ts-expect-error
input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
0) as number,
read: input.usage.cachedInputTokens ?? 0,
read: cachedInputTokens,
},
}
return {
@@ -396,6 +402,9 @@ export namespace Session {
.add(new Decimal(tokens.output).mul(input.model.cost?.output ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.read).mul(input.model.cost?.cache_read ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.write).mul(input.model.cost?.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(input.model.cost?.output ?? 0).div(1_000_000))
.toNumber(),
tokens,
}
+49 -45
View File
@@ -145,6 +145,54 @@ export namespace SessionPrompt {
),
})
export type PromptInput = z.infer<typeof PromptInput>
export async function resolvePromptParts(template: string): Promise<PromptInput["parts"]> {
const parts: PromptInput["parts"] = [
{
type: "text",
text: template,
},
]
const files = ConfigMarkdown.files(template)
await Promise.all(
files.map(async (match) => {
const name = match[1]
const filepath = name.startsWith("~/")
? path.join(os.homedir(), name.slice(2))
: path.resolve(Instance.worktree, name)
const stats = await fs.stat(filepath).catch(() => undefined)
if (!stats) {
const agent = await Agent.get(name)
if (agent) {
parts.push({
type: "agent",
name: agent.name,
})
}
return
}
if (stats.isDirectory()) {
parts.push({
type: "file",
url: `file://${filepath}`,
filename: name,
mime: "application/x-directory",
})
return
}
parts.push({
type: "file",
url: `file://${filepath}`,
filename: name,
mime: "text/plain",
})
}),
)
return parts
}
export async function prompt(input: PromptInput): Promise<MessageV2.WithParts> {
const l = log.clone().tag("session", input.sessionID)
l.info("prompt")
@@ -1605,51 +1653,7 @@ export namespace SessionPrompt {
}
template = template.trim()
const parts = [
{
type: "text",
text: template,
},
] as PromptInput["parts"]
const files = ConfigMarkdown.files(template)
await Promise.all(
files.map(async (match) => {
const name = match[1]
const filepath = name.startsWith("~/")
? path.join(os.homedir(), name.slice(2))
: path.resolve(Instance.worktree, name)
const stats = await fs.stat(filepath).catch(() => undefined)
if (!stats) {
const agent = await Agent.get(name)
if (agent) {
parts.push({
type: "agent",
name: agent.name,
})
}
return
}
if (stats.isDirectory()) {
parts.push({
type: "file",
url: `file://${filepath}`,
filename: name,
mime: "application/x-directory",
})
return
}
parts.push({
type: "file",
url: `file://${filepath}`,
filename: name,
mime: "text/plain",
})
}),
)
const parts = await resolvePromptParts(template)
const model = await (async () => {
if (command.model) {
+4 -3
View File
@@ -170,7 +170,8 @@ export namespace Storage {
const target = path.join(dir, ...key) + ".json"
return withErrorHandling(async () => {
using _ = await Lock.read(target)
return Bun.file(target).json() as Promise<T>
const result = await Bun.file(target).json()
return result as T
})
}
@@ -178,7 +179,7 @@ export namespace Storage {
const dir = await state().then((x) => x.dir)
const target = path.join(dir, ...key) + ".json"
return withErrorHandling(async () => {
using _ = await Lock.write("storage")
using _ = await Lock.write(target)
const content = await Bun.file(target).json()
fn(content)
await Bun.write(target, JSON.stringify(content, null, 2))
@@ -190,7 +191,7 @@ export namespace Storage {
const dir = await state().then((x) => x.dir)
const target = path.join(dir, ...key) + ".json"
return withErrorHandling(async () => {
using _ = await Lock.write("storage")
using _ = await Lock.write(target)
await Bun.write(target, JSON.stringify(content, null, 2))
})
}
+2 -7
View File
@@ -65,6 +65,7 @@ export const TaskTool = Tool.define("task", async () => {
ctx.abort.addEventListener("abort", () => {
SessionLock.abort(session.id)
})
const promptParts = await SessionPrompt.resolvePromptParts(params.prompt)
const result = await SessionPrompt.prompt({
messageID,
sessionID: session.id,
@@ -79,13 +80,7 @@ export const TaskTool = Tool.define("task", async () => {
task: false,
...agent.tools,
},
parts: [
{
id: Identifier.ascending("part"),
type: "text",
text: params.prompt,
},
],
parts: promptParts,
})
unsub()
let all
+19
View File
@@ -0,0 +1,19 @@
export namespace Color {
export function isValidHex(hex?: string): hex is string {
if (!hex) return false
return /^#[0-9a-fA-F]{6}$/.test(hex)
}
export function hexToRgb(hex: string): { r: number; g: number; b: number } {
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
return { r, g, b }
}
export function hexToAnsiBold(hex?: string): string | undefined {
if (!isValidHex(hex)) return undefined
const { r, g, b } = hexToRgb(hex)
return `\x1b[38;2;${r};${g};${b}m\x1b[1m`
}
}
@@ -0,0 +1,66 @@
import { test, expect } from "bun:test"
import path from "path"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { Config } from "../../src/config/config"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { Color } from "../../src/util/color"
test("agent color parsed from project config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
agent: {
build: { color: "#FFA500" },
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const cfg = await Config.get()
expect(cfg.agent?.["build"]?.color).toBe("#FFA500")
},
})
})
test("Agent.get includes color from config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
agent: {
plan: { color: "#A855F7" },
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const plan = await AgentSvc.get("plan")
expect(plan?.color).toBe("#A855F7")
},
})
})
test("Color.hexToAnsiBold converts valid hex to ANSI", () => {
const result = Color.hexToAnsiBold("#FFA500")
expect(result).toBe("\x1b[38;2;255;165;0m\x1b[1m")
})
test("Color.hexToAnsiBold returns undefined for invalid hex", () => {
expect(Color.hexToAnsiBold(undefined)).toBeUndefined()
expect(Color.hexToAnsiBold("")).toBeUndefined()
expect(Color.hexToAnsiBold("#FFF")).toBeUndefined()
expect(Color.hexToAnsiBold("FFA500")).toBeUndefined()
expect(Color.hexToAnsiBold("#GGGGGG")).toBeUndefined()
})
@@ -0,0 +1,40 @@
import { describe, expect, test } from "bun:test"
import { Project } from "../../src/project/project"
import { Log } from "../../src/util/log"
import { $ } from "bun"
import path from "path"
import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
describe("Project.fromDirectory", () => {
test("should handle git repository with no commits", async () => {
await using tmp = await tmpdir()
await $`git init`.cwd(tmp.path).quiet()
const project = await Project.fromDirectory(tmp.path)
expect(project).toBeDefined()
expect(project.id).toBe("global")
expect(project.worktree).toBe("/")
const opencodeFile = path.join(tmp.path, ".git", "opencode")
const fileExists = await Bun.file(opencodeFile).exists()
expect(fileExists).toBe(false)
})
test("should handle git repository with commits", async () => {
await using tmp = await tmpdir({ git: true })
const project = await Project.fromDirectory(tmp.path)
expect(project).toBeDefined()
expect(project.id).not.toBe("global")
expect(project.vcs).toBe("git")
expect(project.worktree).toBe(tmp.path)
const opencodeFile = path.join(tmp.path, ".git", "opencode")
const fileExists = await Bun.file(opencodeFile).exists()
expect(fileExists).toBe(true)
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.58",
"version": "1.0.62",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+2
View File
@@ -49,6 +49,7 @@ type Agent struct {
Options map[string]interface{} `json:"options,required"`
Permission AgentPermission `json:"permission,required"`
Tools map[string]bool `json:"tools,required"`
Color string `json:"color"`
Description string `json:"description"`
Model AgentModel `json:"model"`
Prompt string `json:"prompt"`
@@ -65,6 +66,7 @@ type agentJSON struct {
Options apijson.Field
Permission apijson.Field
Tools apijson.Field
Color apijson.Field
Description apijson.Field
Model apijson.Field
Prompt apijson.Field
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.58",
"version": "1.0.62",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+5
View File
@@ -190,6 +190,10 @@ export type AgentConfig = {
*/
description?: string
mode?: "subagent" | "primary" | "all"
/**
* Hex color code for the agent (e.g., #FF5733)
*/
color?: string
permission?: {
edit?: "ask" | "allow" | "deny"
bash?:
@@ -1043,6 +1047,7 @@ export type Agent = {
builtIn: boolean
topP?: number
temperature?: number
color?: string
permission: {
edit: "ask" | "allow" | "deny"
bash: {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.58",
"version": "1.0.62",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.58",
"version": "1.0.62",
"type": "module",
"exports": {
".": "./src/components/index.ts",
+23 -15
View File
@@ -12,9 +12,6 @@
align-items: flex-start;
gap: 0px;
align-self: stretch;
border: 1px solid var(--border-weak-base);
border-bottom: none;
border-top: none;
overflow: clip;
[data-slot="accordion-header"] {
@@ -36,7 +33,7 @@
user-select: none;
background-color: var(--surface-base);
border-bottom: 1px solid var(--border-weak-base);
border: 1px solid var(--border-weak-base);
overflow: clip;
color: var(--text-strong);
transition: background-color 0.15s ease;
@@ -62,11 +59,19 @@
}
&[data-expanded] {
border: 1px solid var(--border-weak-base);
border-bottom: 1px solid var(--border-weak-base);
margin-top: 8px;
margin-bottom: 8px;
border-radius: 8px;
[data-slot="accordion-trigger"] {
border-radius: 8px 8px 0 0;
}
[data-slot="accordion-content"] {
border: 1px solid var(--border-weak-base);
border-top: none;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
[data-slot="accordion-item"]:has(+ &) {
&[data-closed] {
@@ -81,18 +86,23 @@
}
& + [data-slot="accordion-item"] {
border-top: 1px solid var(--border-weak-base);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
margin-top: 8px;
[data-slot="accordion-trigger"] {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
}
}
&[data-closed] + &[data-closed] {
[data-slot="accordion-trigger"] {
border-top: none;
}
}
&:first-child {
margin-top: 0px;
border-top: 1px solid var(--border-weak-base);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
&[data-closed] {
[data-slot="accordion-trigger"] {
@@ -104,8 +114,6 @@
&:last-child {
margin-bottom: 0px;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
&[data-closed] {
[data-slot="accordion-trigger"] {
+2 -2
View File
@@ -77,7 +77,7 @@
--background-weak: var(--smoke-light-3);
--background-strong: var(--smoke-light-1);
--background-stronger: #fcfcfc;
--surface-base: var(--smoke-light-alpha-2);
--surface-base: var(--smoke-light-3);
--base: var(--smoke-light-alpha-2);
--surface-base-hover: #0500000f;
--surface-base-active: var(--smoke-light-alpha-3);
@@ -317,7 +317,7 @@
--background-weak: #1b1818;
--background-strong: #151313;
--background-stronger: #191515;
--surface-base: var(--smoke-dark-alpha-2);
--surface-base: var(--smoke-dark-3);
--base: var(--smoke-dark-alpha-2);
--surface-base-hover: #e0b7b716;
--surface-base-active: var(--smoke-dark-alpha-3);
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.58",
"version": "1.0.62",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+5 -5
View File
@@ -16,7 +16,7 @@ OpenCode supports both **JSON** and **JSONC** (JSON with Comments) formats.
"$schema": "https://opencode.ai/config.json",
// Theme configuration
"theme": "opencode",
"model": "anthropic/claude-sonnet-4-20250514",
"model": "anthropic/claude-sonnet-4-5",
"autoupdate": true,
}
```
@@ -128,8 +128,8 @@ You can configure the providers and models you want to use in your OpenCode conf
{
"$schema": "https://opencode.ai/config.json",
"provider": {},
"model": "anthropic/claude-sonnet-4-20250514",
"small_model": "anthropic/claude-3-5-haiku-20241022"
"model": "anthropic/claude-sonnet-4-5",
"small_model": "anthropic/claude-haiku-4-5"
}
```
@@ -164,7 +164,7 @@ You can configure specialized agents for specific tasks through the `agent` opti
"agent": {
"code-reviewer": {
"description": "Reviews code for best practices and potential issues",
"model": "anthropic/claude-sonnet-4-20250514",
"model": "anthropic/claude-sonnet-4-5",
"prompt": "You are a code reviewer. Focus on security, performance, and maintainability.",
"tools": {
// Disable file modification tools for review-only agent
@@ -213,7 +213,7 @@ You can configure custom commands for repetitive tasks through the `command` opt
"template": "Run the full test suite with coverage report and show any failures.\nFocus on the failing tests and suggest fixes.",
"description": "Run tests with coverage",
"agent": "build",
"model": "anthropic/claude-3-5-sonnet-20241022",
"model": "anthropic/claude-haiku-4-5",
},
"component": {
"template": "Create a new React component named $ARGUMENTS with TypeScript support.\nInclude proper typing and basic structure.",
+19 -18
View File
@@ -11,24 +11,25 @@ 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 |
| 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 servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
@@ -256,7 +256,7 @@ Below are examples of some common MCP servers. You can submit a PR if you want t
### Context7
Add the [Context7 MCP server](https://github.com/context-labs/mcp-server-context7) to search through docs.
Add the [Context7 MCP server](https://github.com/upstash/context7) to search through docs.
```json title="opencode.json" {4-7}
{
@@ -783,6 +783,42 @@ OpenCode Zen is a list of tested and verified models provided by the OpenCode te
---
### OVHcloud AI Endpoints
1. Head over to the [OVHcloud panel](https://ovh.com/manager). Navigate to the `Public Cloud` section, `AI & Machine Learning` > `AI Endpoints` and in `API Keys` tab, click **Create a new API key**.
2. Run `opencode auth login` and select **OVHcloud AI Endpoints**.
```bash
$ opencode auth login
┌ Add credential
◆ Select provider
│ ● OVHcloud AI Endpoints
│ ...
```
3. Enter your OVHcloud AI Endpoints API key.
```bash
$ opencode auth login
┌ Add credential
◇ Select provider
│ OVHcloud AI Endpoints
◇ Enter your API key
│ _
```
4. Run the `/models` command to select a model like _gpt-oss-120b_.
---
### Together AI
1. Head over to the [Together AI console](https://api.together.ai), create an account, and click **Add Key**.
+4 -1
View File
@@ -49,6 +49,7 @@ if (!Script.preview) {
- Do NOT make general statements about "improvements", be very specific about what was changed.
- Do NOT include any information about code changes if they do not affect the user facing changes.
- For commits that are already well-written and descriptive, avoid rewording them. Simply capitalize the first letter, fix any misspellings, and ensure proper English grammar.
- DO NOT read any other commits than the ones listed above (THIS IS IMPORTANT TO AVOID DUPLICATING THINGS IN OUR CHANGELOG)
IMPORTANT: ONLY return a bulleted list of changes, do not include any other information. Do not include a preamble like "Based on my analysis..."
@@ -67,7 +68,9 @@ if (!Script.preview) {
notes.push(line)
}
}
console.log(notes)
console.log("---- Generated Changelog ----")
console.log(notes.join("\n"))
console.log("-----------------------------")
opencode.server.close()
}
+3 -4
View File
@@ -7,7 +7,6 @@ import { join } from "path"
const FORK_REPO = "sst/zed-extensions"
const UPSTREAM_REPO = "zed-industries/extensions"
const EXTENSION_NAME = "opencode"
const OPENCODE_REPO = "sst/opencode"
async function main() {
const version = process.argv[2]
@@ -40,8 +39,8 @@ async function main() {
process.chdir(workDir)
// Configure git identity
await $`git config user.name "github-actions[bot]"`
await $`git config user.email "github-actions[bot]@users.noreply.github.com"`
await $`git config user.name "Dax"`
await $`git config user.email "mail@thdxr.com"`
// Sync fork with upstream
console.log(`🔄 Syncing fork with upstream...`)
@@ -108,7 +107,7 @@ async function main() {
console.log(`📬 Creating pull request...`)
const prUrl =
await $`gh pr create --repo ${UPSTREAM_REPO} --base main --head ${FORK_REPO.split("/")[0]}:${branchName} --title "Update ${EXTENSION_NAME} to v${cleanVersion}" --body "Release notes:\n\nhttps://github.com/${OPENCODE_REPO}/releases/tag/v${cleanVersion}"`.text()
await $`gh pr create --repo ${UPSTREAM_REPO} --base main --head ${FORK_REPO.split("/")[0]}:${branchName} --title "Update ${EXTENSION_NAME} to v${cleanVersion}" --body "Updating OpenCode extension to v${cleanVersion}"`.text()
console.log(`✅ Pull request created: ${prUrl}`)
console.log(`🎉 Done!`)
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.58",
"version": "1.0.62",
"publisher": "sst-dev",
"repository": {
"type": "git",