Merge branch 'dev' into fix/cli-clean-exit-on-model-errors
This commit is contained in:
commit
2048f32491
43 changed files with 942 additions and 253 deletions
223
.opencode/themes/mytheme.json
Normal file
223
.opencode/themes/mytheme.json
Normal 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
STATS.md
1
STATS.md
|
|
@ -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) |
|
||||
|
|
|
|||
42
bun.lock
42
bun.lock
|
|
@ -40,7 +40,7 @@
|
|||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.58",
|
||||
"version": "1.0.61",
|
||||
"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.61",
|
||||
"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.61",
|
||||
"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.61",
|
||||
"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.61",
|
||||
"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.61",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
|
|
@ -189,8 +189,8 @@
|
|||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opentui/core": "0.1.41",
|
||||
"@opentui/solid": "0.1.41",
|
||||
"@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.61",
|
||||
"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.61",
|
||||
"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.61",
|
||||
"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.61",
|
||||
"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.61",
|
||||
"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.1.41", "", { "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.41", "@opentui/core-darwin-x64": "0.1.41", "@opentui/core-linux-arm64": "0.1.41", "@opentui/core-linux-x64": "0.1.41", "@opentui/core-win32-arm64": "0.1.41", "@opentui/core-win32-x64": "0.1.41", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-G1v5ZsE7AozKz38ffsxiMuF3tv7hWc+JgqZTyEEMQ57GmGoBrSTNKdWTBNbHy/V9asteIDdBLJV3h1RJedjDCA=="],
|
||||
"@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.1.41", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UZqZBiMHUFpYlozJePtNWdJnnhzJL7IaYSyYQVeUHi+0AH10EzEMASLYlYFRWKofCvVgECm/BIhn9PMGZodCwA=="],
|
||||
"@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.1.41", "", { "os": "darwin", "cpu": "x64" }, "sha512-cgbZPAMWwALCHtIj+C2ZUpLdRXFWCF7PJqqN9q+psg2sSXBZRPD3oPvonU9hVordMUKDVTjngsBqNo76mzFZGg=="],
|
||||
"@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.1.41", "", { "os": "linux", "cpu": "arm64" }, "sha512-kKBiuM+tb6OGSWObD6W/0IXwofT6tAvlnWA6zje5j7HUFsoo1I0jzynUGCjzOlytsHU56XVWM5dLYd+t1gMr4w=="],
|
||||
"@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.1.41", "", { "os": "linux", "cpu": "x64" }, "sha512-BVjuXPEJdz0nu+sSxF9noP7XCrbE1ZEt5xNcKJ3AvLUCbmwYzV7Z/5hscfa/kJErSln0USpCkixVMpoCrbvlnA=="],
|
||||
"@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.1.41", "", { "os": "win32", "cpu": "arm64" }, "sha512-ojGXe1IfT6BrsQxn0wtr3zJIwLqRlABNyHUwPv45qJ/Nwl058ZO40HijjUo7cHKaQzuMJ6uzP1J2yqo3GHn9YQ=="],
|
||||
"@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.1.41", "", { "os": "win32", "cpu": "x64" }, "sha512-KtDYXdFUHEpbyYUdV3LRctkCC3NIwCMDAnHlzFnbsnwJ4NewM9jUtDGDq9gt8ysK48rotrSwbr9udvGAJJel3g=="],
|
||||
"@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.1.41", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.41", "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-zmbMKY2X1/GKRsEWy+pjyNfap2Wlj/7RNmk1DXC8fon3exSeLotyC3mjLT1rpZya+l5pVdyFCj1Bjbg82i028Q=="],
|
||||
"@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=="],
|
||||
|
||||
|
|
|
|||
|
|
@ -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.61"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.58",
|
||||
"version": "1.0.61",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.58",
|
||||
"version": "1.0.61",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.58",
|
||||
"version": "1.0.61",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.58",
|
||||
"version": "1.0.61",
|
||||
"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) => (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The AI coding agent built for the terminal"
|
||||
version = "1.0.58"
|
||||
version = "1.0.61"
|
||||
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.61/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.61/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.61/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.61/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.61/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.58",
|
||||
"version": "1.0.61",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.58",
|
||||
"version": "1.0.61",
|
||||
"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.1.41",
|
||||
"@opentui/solid": "0.1.41",
|
||||
"@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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
112
packages/opencode/src/cli/cmd/pr.ts
Normal file
112
packages/opencode/src/cli/cmd/pr.ts
Normal 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)
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -228,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()
|
||||
},
|
||||
},
|
||||
|
|
@ -252,6 +262,7 @@ export function Prompt(props: PromptProps) {
|
|||
prompt: PromptInfo
|
||||
mode: "normal" | "shell"
|
||||
extmarkToPartIndex: Map<number, number>
|
||||
interrupt: number
|
||||
}>({
|
||||
prompt: {
|
||||
input: "",
|
||||
|
|
@ -259,6 +270,7 @@ export function Prompt(props: PromptProps) {
|
|||
},
|
||||
mode: "normal",
|
||||
extmarkToPartIndex: new Map(),
|
||||
interrupt: 0,
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
|
|
@ -746,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()
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -757,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] ?? []}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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") ||
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ export namespace Server {
|
|||
return c.json(await Config.get())
|
||||
},
|
||||
)
|
||||
|
||||
.patch(
|
||||
"/config",
|
||||
describeRoute({
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
packages/opencode/src/util/color.ts
Normal file
19
packages/opencode/src/util/color.ts
Normal 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`
|
||||
}
|
||||
}
|
||||
66
packages/opencode/test/config/agent-color.test.ts
Normal file
66
packages/opencode/test/config/agent-color.test.ts
Normal file
|
|
@ -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()
|
||||
})
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.58",
|
||||
"version": "1.0.61",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
|
|||
|
|
@ -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,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.58",
|
||||
"version": "1.0.61",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
|
|||
|
|
@ -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,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.58",
|
||||
"version": "1.0.61",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.58",
|
||||
"version": "1.0.61",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/components/index.ts",
|
||||
|
|
|
|||
|
|
@ -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"] {
|
||||
|
|
|
|||
|
|
@ -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,7 +1,7 @@
|
|||
{
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"version": "1.0.58",
|
||||
"version": "1.0.61",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.0.58",
|
||||
"version": "1.0.61",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue