Compare commits

...
39 Commits
Author SHA1 Message Date
opencode c9c9db1e8d release: v0.5.11 2025-08-20 16:36:05 +00:00
Dax Raad b11fe9fbc6 ignore: remove import 2025-08-20 12:29:24 -04:00
Dax Raad 60f3d413de remove auto browser open for now 2025-08-20 12:28:00 -04:00
opencode 1df2d78b85 release: v0.5.10 2025-08-20 16:12:00 +00:00
opencode 2286a872c1 release: v0.5.9 2025-08-20 15:51:24 +00:00
Dax Raad 8a83301e0d copilot auth update version 2025-08-20 11:46:14 -04:00
GitHub Action 9bc40f00e3 ignore: update download stats 2025-08-20 2025-08-20 12:04:23 +00:00
opencode c3c440948a release: v0.5.8 2025-08-20 05:08:31 +00:00
Dax Raad aa10f8a7f6 sonic model 2025-08-20 01:02:41 -04:00
Aiden ClineandGitHub a2db58f125 fix: don't let --continue access subagent session (#2091) 2025-08-19 22:40:07 -05:00
Aiden ClineandGitHub 574be9febf fix: keybind panic (#2092) 2025-08-19 22:39:59 -05:00
Aiden ClineandGitHub 5b05ede748 fix: agent casing issue (#2081) 2025-08-19 18:08:56 -05:00
Aiden ClineandGitHub 4032426185 docs: remove non existent keybind (#2080) 2025-08-19 17:39:02 -05:00
Jay V 8d8045ff95 docs: add sdk doc 2025-08-19 18:11:36 -04:00
Jay V b3c8bec019 docs: edit server 2025-08-19 17:21:45 -04:00
Aiden ClineandGitHub 25f43adaa0 tweak: notify agent it is in build mode when switching from plan mode (#2065) 2025-08-19 15:32:31 -05:00
Timo ClasenandGitHub 4913ee6afd fix(TUI): make it less shimmer (#2076) 2025-08-19 15:30:54 -05:00
c59ded82b3 docs: document server API endpoints (#2019)
Co-authored-by: Jay <air@live.ca>
2025-08-19 16:13:02 -04:00
Aiden ClineandGitHub 40bdbf92a3 fix: tui panic from logger (#2075) 2025-08-19 14:47:44 -05:00
Aiden ClineGitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
ad76d7e57d fix: add type checking for MCP tool path parameters (#2073)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-08-19 13:38:33 -05:00
GitHub Action 863ae6fa7d ignore: update download stats 2025-08-19 2025-08-19 12:04:15 +00:00
Aiden ClineandGitHub 8f230ad4b4 fix: interface conversion panic (#2060) 2025-08-19 05:25:46 -05:00
Aiden ClineandGitHub c0f90eb564 tweak: better agent create error handling (#2058) 2025-08-19 00:14:50 -05:00
DaxandGitHub 50fb337270 Update duplicate-issues.yml 2025-08-18 20:00:59 -04:00
Aiden ClineandGitHub e08ec077b0 fix: ensure name isn't added as field in options: {...} (#2053) 2025-08-18 18:15:20 -05:00
Aiden ClineandGitHub 796245d146 blacklist gpt-5-chat-latest (#2048) 2025-08-18 17:50:38 -04:00
opencode 303a1044a8 release: v0.5.7 2025-08-18 21:43:17 +00:00
DaxandGitHub f19586cebd fix anthropic console auth (#2049) 2025-08-18 17:12:21 -04:00
Jay V 5d12cadba7 docs:edit 2025-08-18 13:52:53 -04:00
Jay V 745988f9e3 docs:edit 2025-08-18 13:51:08 -04:00
Jay V 61580e6dce docs: edits 2025-08-18 13:31:01 -04:00
Jay V 2dea8f0f6b docs: add tui doc 2025-08-18 13:31:01 -04:00
opencode 446ce488c0 release: v0.5.6 2025-08-18 15:56:22 +00:00
John ConnorandGitHub 21b000aed0 Remove redundant line from agents.mdx (#2031) 2025-08-18 08:34:57 -05:00
GitHub Action 0cdd8be70a ignore: update download stats 2025-08-18 2025-08-18 12:04:33 +00:00
adamdotdevin 2f4db2777c fix(tui): title bg color missing on system theme 2025-08-18 06:00:38 -05:00
YtzhakandGitHub 667ff90dd6 feat: add shimmer text rendering (#2027) 2025-08-18 05:55:01 -05:00
spoons-and-mirrorsandGitHub cd3d91209a tweak(timeline): add a dot to the session timeline modal for better visual cue of session's revert point (#1978) 2025-08-18 05:50:43 -05:00
Frank 75ed131abf sync 2025-08-18 07:58:46 +00:00
51 changed files with 1391 additions and 371 deletions
+1 -3
View File
@@ -36,10 +36,8 @@ jobs:
Issue number: Issue number:
${{ github.event.issue.number }} ${{ github.event.issue.number }}
Issue body:
${{ github.event.issue.body }}
Please search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue. Lookup this issue and search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
Consider: Consider:
1. Similar titles or descriptions 1. Similar titles or descriptions
2. Same error messages or symptoms 2. Same error messages or symptoms
+20 -5
View File
@@ -1,6 +1,4 @@
--- ---
model: openai/gpt-5
reasoningEffort: medium
description: ALWAYS use this when writing docs description: ALWAYS use this when writing docs
--- ---
@@ -8,7 +6,24 @@ You are an expert technical documentation writer
You are not verbose You are not verbose
Every chunk of text should be followed by an example or something besides text The title of the page should be a word or a 2-3 word phrase
to look at.
Chunks of text should not be more than 2 sentences long. The description should be one short line, should not start with "The", should
avoid repeating the title of the page, should be 5-10 words long
Chunks of text should not be more than 2 sentences long
Each section is spearated by a divider of 3 dashes
The section titles are short with only the first letter of the word capitalized
The section titles are in the imperative mood
The section titles should not repeat the term used in the page title, for
example, if the page title is "Models", avoid using a section title like "Add
new models". This might be unavoidable in some cases, but try to avoid it.
Check out the /packages/web/src/content/docs/docs/index.mdx as an example.
For JS or TS code snippets remove trailing semicolons and any trailing commas
that might not be needed.
+3
View File
@@ -51,3 +51,6 @@
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) | | 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) | | 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) | | 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
+11 -74
View File
@@ -15,6 +15,7 @@
"name": "@opencode/cloud-app", "name": "@opencode/cloud-app",
"dependencies": { "dependencies": {
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@solidjs/meta": "^0.29.4", "@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0", "@solidjs/router": "^0.15.0",
"@solidjs/start": "^1.1.0", "@solidjs/start": "^1.1.0",
@@ -24,7 +25,7 @@
}, },
"cloud/core": { "cloud/core": {
"name": "@opencode/cloud-core", "name": "@opencode/cloud-core",
"version": "0.5.4", "version": "0.5.5",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"drizzle-orm": "0.41.0", "drizzle-orm": "0.41.0",
@@ -38,7 +39,7 @@
}, },
"cloud/function": { "cloud/function": {
"name": "@opencode/cloud-function", "name": "@opencode/cloud-function",
"version": "0.5.4", "version": "0.5.5",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "2.0.0", "@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2", "@ai-sdk/openai": "2.0.2",
@@ -58,7 +59,7 @@
}, },
"cloud/web": { "cloud/web": {
"name": "@opencode/cloud-web", "name": "@opencode/cloud-web",
"version": "0.5.4", "version": "0.5.5",
"dependencies": { "dependencies": {
"@kobalte/core": "0.13.9", "@kobalte/core": "0.13.9",
"@openauthjs/solid": "0.0.0-20250322224806", "@openauthjs/solid": "0.0.0-20250322224806",
@@ -77,7 +78,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode/function", "name": "@opencode/function",
"version": "0.5.4", "version": "0.5.5",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
@@ -92,18 +93,14 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "0.5.4", "version": "0.5.5",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
"dependencies": { "dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@clack/prompts": "1.0.0-alpha.1", "@clack/prompts": "1.0.0-alpha.1",
"@hono/zod-validator": "catalog:", "@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.15.1", "@modelcontextprotocol/sdk": "1.15.1",
"@octokit/graphql": "9.0.1",
"@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.4.3", "@openauthjs/openauth": "0.4.3",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -146,7 +143,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "0.5.4", "version": "0.5.5",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
}, },
@@ -158,7 +155,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "0.5.4", "version": "0.5.5",
"devDependencies": { "devDependencies": {
"@hey-api/openapi-ts": "0.80.1", "@hey-api/openapi-ts": "0.80.1",
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
@@ -167,7 +164,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode/web", "name": "@opencode/web",
"version": "0.5.4", "version": "0.5.5",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@@ -217,16 +214,6 @@
"zod": "3.25.76", "zod": "3.25.76",
}, },
"packages": { "packages": {
"@actions/core": ["@actions/core@1.11.1", "", { "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" } }, "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A=="],
"@actions/exec": ["@actions/exec@1.1.1", "", { "dependencies": { "@actions/io": "^1.0.1" } }, "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w=="],
"@actions/github": ["@actions/github@6.0.1", "", { "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", "@octokit/plugin-paginate-rest": "^9.2.2", "@octokit/plugin-rest-endpoint-methods": "^10.4.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "undici": "^5.28.5" } }, "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw=="],
"@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@2.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="], "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@2.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="],
@@ -485,7 +472,7 @@
"@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.3", "", { "dependencies": { "@expressive-code/core": "^0.41.3" } }, "sha512-SN8tkIzDpA0HLAscEYD2IVrfLiid6qEdE9QLlGVSxO1KEw7qYvjpbNBQjUjMr5/jvTJ7ys6zysU2vLPHE0sb2g=="], "@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.3", "", { "dependencies": { "@expressive-code/core": "^0.41.3" } }, "sha512-SN8tkIzDpA0HLAscEYD2IVrfLiid6qEdE9QLlGVSxO1KEw7qYvjpbNBQjUjMr5/jvTJ7ys6zysU2vLPHE0sb2g=="],
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], "@fastify/busboy": ["@fastify/busboy@3.1.1", "", {}, "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw=="],
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
@@ -1469,8 +1456,6 @@
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
@@ -2843,8 +2828,6 @@
"tuf-js": ["tuf-js@2.2.1", "", { "dependencies": { "@tufjs/models": "2.0.1", "debug": "^4.3.4", "make-fetch-happen": "^13.0.1" } }, "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA=="], "tuf-js": ["tuf-js@2.2.1", "", { "dependencies": { "@tufjs/models": "2.0.1", "debug": "^4.3.4", "make-fetch-happen": "^13.0.1" } }, "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA=="],
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="], "turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
@@ -2869,7 +2852,7 @@
"unctx": ["unctx@2.4.1", "", { "dependencies": { "acorn": "^8.14.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.17", "unplugin": "^2.1.0" } }, "sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg=="], "unctx": ["unctx@2.4.1", "", { "dependencies": { "acorn": "^8.14.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.17", "unplugin": "^2.1.0" } }, "sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg=="],
"undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], "undici": ["undici@7.13.0", "", {}, "sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
@@ -3063,16 +3046,6 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@actions/github/@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="],
"@actions/github/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="],
"@actions/github/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@10.4.1", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg=="],
"@actions/github/@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="],
"@actions/github/@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
@@ -3347,8 +3320,6 @@
"@whatwg-node/fetch/urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="], "@whatwg-node/fetch/urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
"@whatwg-node/node-fetch/@fastify/busboy": ["@fastify/busboy@3.1.1", "", {}, "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw=="],
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g=="], "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -3443,8 +3414,6 @@
"miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], "miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
"miniflare/undici": ["undici@7.13.0", "", {}, "sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA=="],
"miniflare/youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], "miniflare/youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
"miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
@@ -3615,28 +3584,6 @@
"zip-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], "zip-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
"@actions/github/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="],
"@actions/github/@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="],
"@actions/github/@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
"@actions/github/@octokit/core/before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="],
"@actions/github/@octokit/core/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
"@actions/github/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
"@actions/github/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="],
"@actions/github/@octokit/request/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
"@actions/github/@octokit/request/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
"@actions/github/@octokit/request-error/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
@@ -4021,16 +3968,6 @@
"zip-stream/readable-stream/events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], "zip-stream/readable-stream/events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"@actions/github/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
"@actions/github/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
"@actions/github/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
"@actions/github/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
+2 -1
View File
@@ -5,10 +5,11 @@
"dev": "vinxi dev --host 0.0.0.0", "dev": "vinxi dev --host 0.0.0.0",
"build": "vinxi build", "build": "vinxi build",
"start": "vinxi start", "start": "vinxi start",
"version": "0.5.5" "version": "0.5.11"
}, },
"dependencies": { "dependencies": {
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@solidjs/meta": "^0.29.4", "@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0", "@solidjs/router": "^0.15.0",
"@solidjs/start": "^1.1.0", "@solidjs/start": "^1.1.0",
+28
View File
@@ -0,0 +1,28 @@
import { useSession } from "vinxi/http"
import { createClient } from "@openauthjs/openauth/client"
export const AuthClient = createClient({
clientID: "app",
issuer: "https://auth.dev.opencode.ai",
})
export interface AuthSession {
account: Record<string, {
id: string
email: string
}>
current?: string
}
export function useAuthSession() {
"use server"
return useSession<AuthSession>({
password: "0".repeat(32),
name: "auth"
})
}
export function AuthProvider() {
}
+7
View File
@@ -0,0 +1,7 @@
import type { APIEvent } from "@solidjs/start/server"
import { AuthClient } from "~/context/auth"
export async function GET(input: APIEvent) {
const result = await AuthClient.authorize(new URL("./callback", input.request.url).toString(), "code")
return Response.redirect(result.url, 302)
}
+36
View File
@@ -0,0 +1,36 @@
import type { APIEvent } from "@solidjs/start/server"
import { AuthClient, useAuthSession } from "~/context/auth"
export async function GET(input: APIEvent) {
const url = new URL(input.request.url)
const code = url.searchParams.get("code")
if (!code) throw new Error("No code found")
const redirectURI = `${url.origin}${url.pathname}`
console.log({
redirectURI,
code,
})
const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
if (result.err) {
throw new Error(result.err.message)
}
const decoded = AuthClient.decode(result.tokens.access, {} as any)
if (decoded.err) throw new Error(decoded.err.message)
const session = await useAuthSession()
const id = decoded.subject.properties.accountID
await session.update((value) => {
return {
...value,
account: {
[id]: {
id,
email: decoded.subject.properties.email,
},
},
current: id,
}
})
return {
result,
}
}
+1 -1
View File
@@ -51,7 +51,7 @@ export default function Home() {
<a href="/docs">Get Started</a> <a href="/docs">Get Started</a>
</div> </div>
<div data-slot="right"> <div data-slot="right">
<button data-copy data-slot="command" data-command="curl -fsSL https://opencode.ai/install | bash"> <button data-copy data-slot="command">
<span> <span>
<span>curl -fsSL&nbsp;</span> <span>curl -fsSL&nbsp;</span>
<span data-slot="protocol">https://</span> <span data-slot="protocol">https://</span>
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cloud-core", "name": "@opencode/cloud-core",
"version": "0.5.5", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode/cloud-function", "name": "@opencode/cloud-function",
"version": "0.5.5", "version": "0.5.11",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode/cloud-web", "name": "@opencode/cloud-web",
"version": "0.5.5", "version": "0.5.11",
"private": true, "private": true,
"description": "", "description": "",
"type": "module", "type": "module",
+1 -1
View File
@@ -8,7 +8,7 @@
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@octokit/graphql": "9.0.1", "@octokit/graphql": "9.0.1",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
"@opencode-ai/sdk": "^0.5.4", "@opencode-ai/sdk": "0.5.4",
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode/function", "name": "@opencode/function",
"version": "0.5.5", "version": "0.5.11",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "0.5.5", "version": "0.5.11",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"private": true, "private": true,
+3 -1
View File
@@ -100,7 +100,7 @@ export namespace Agent {
tools: {}, tools: {},
builtIn: false, builtIn: false,
} }
const { model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value const { name, model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
item.options = { item.options = {
...item.options, ...item.options,
...extra, ...extra,
@@ -120,6 +120,8 @@ export namespace Agent {
if (temperature != undefined) item.temperature = temperature if (temperature != undefined) item.temperature = temperature
if (top_p != undefined) item.topP = top_p if (top_p != undefined) item.topP = top_p
if (mode) item.mode = mode if (mode) item.mode = mode
// just here for consistency & to prevent it from being added as an option
if (name) item.name = name
if (permission ?? cfg.permission) { if (permission ?? cfg.permission) {
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {}) item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
+4 -1
View File
@@ -46,7 +46,10 @@ const AgentCreateCommand = cmd({
const spinner = prompts.spinner() const spinner = prompts.spinner()
spinner.start("Generating agent configuration...") spinner.start("Generating agent configuration...")
const generated = await Agent.generate({ description: query }) const generated = await Agent.generate({ description: query }).catch((error) => {
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
throw new UI.CancelledError()
})
spinner.stop(`Agent ${generated.identifier} generated`) spinner.stop(`Agent ${generated.identifier} generated`)
const availableTools = [ const availableTools = [
+28 -16
View File
@@ -1,7 +1,6 @@
import { Auth } from "../../auth" import { Auth } from "../../auth"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import * as prompts from "@clack/prompts" import * as prompts from "@clack/prompts"
import open from "open"
import { UI } from "../ui" import { UI } from "../ui"
import { ModelsDev } from "../../provider/models" import { ModelsDev } from "../../provider/models"
import { map, pipe, sortBy, values } from "remeda" import { map, pipe, sortBy, values } from "remeda"
@@ -159,9 +158,6 @@ export const AuthLoginCommand = cmd({
const authorize = await method.authorize() const authorize = await method.authorize()
if (authorize.url) { if (authorize.url) {
try {
await open(authorize.url)
} catch (e) {}
prompts.log.info("Go to: " + authorize.url) prompts.log.info("Go to: " + authorize.url)
} }
@@ -176,12 +172,20 @@ export const AuthLoginCommand = cmd({
spinner.stop("Failed to authorize", 1) spinner.stop("Failed to authorize", 1)
} }
if (result.type === "success") { if (result.type === "success") {
await Auth.set(provider, { if ("refresh" in result) {
type: "oauth", await Auth.set(provider, {
refresh: result.refresh, type: "oauth",
access: result.access, refresh: result.refresh,
expires: result.expires, access: result.access,
}) expires: result.expires,
})
}
if ("key" in result) {
await Auth.set(provider, {
type: "api",
key: result.key,
})
}
spinner.stop("Login successful") spinner.stop("Login successful")
} }
} }
@@ -197,12 +201,20 @@ export const AuthLoginCommand = cmd({
prompts.log.error("Failed to authorize") prompts.log.error("Failed to authorize")
} }
if (result.type === "success") { if (result.type === "success") {
await Auth.set(provider, { if ("refresh" in result) {
type: "oauth", await Auth.set(provider, {
refresh: result.refresh, type: "oauth",
access: result.access, refresh: result.refresh,
expires: result.expires, access: result.access,
}) expires: result.expires,
})
}
if ("key" in result) {
await Auth.set(provider, {
type: "api",
key: result.key,
})
}
prompts.log.success("Login successful") prompts.log.success("Login successful")
} }
} }
+11 -5
View File
@@ -67,11 +67,17 @@ export const RunCommand = cmd({
await bootstrap({ cwd: process.cwd() }, async () => { await bootstrap({ cwd: process.cwd() }, async () => {
const session = await (async () => { const session = await (async () => {
if (args.continue) { if (args.continue) {
const list = Session.list() const it = Session.list()
const first = await list.next() try {
await list.return() for await (const s of it) {
if (first.done) return if (s.parentID === undefined) {
return first.value return s
}
}
return
} finally {
await it.return()
}
} }
if (args.session) return Session.get(args.session) if (args.session) return Session.get(args.session)
+11 -5
View File
@@ -82,11 +82,17 @@ export const TuiCommand = cmd({
const result = await bootstrap({ cwd }, async (app) => { const result = await bootstrap({ cwd }, async (app) => {
const sessionID = await (async () => { const sessionID = await (async () => {
if (args.continue) { if (args.continue) {
const list = Session.list() const it = Session.list()
const first = await list.next() try {
await list.return() for await (const s of it) {
if (first.done) return if (s.parentID === undefined) {
return first.value.id return s.id
}
}
return
} finally {
await it.return()
}
} }
if (args.session) { if (args.session) {
return args.session return args.session
+1 -1
View File
@@ -45,7 +45,7 @@ export const UpgradeCommand = {
spinner.start("Upgrading...") spinner.start("Upgrading...")
const err = await Installation.upgrade(method, target).catch((err) => err) const err = await Installation.upgrade(method, target).catch((err) => err)
if (err) { if (err) {
spinner.stop("Upgrade failed") spinner.stop("Upgrade failed", 1)
if (err instanceof Installation.UpgradeFailedError) prompts.log.error(err.data.stderr) if (err instanceof Installation.UpgradeFailedError) prompts.log.error(err.data.stderr)
else if (err instanceof Error) prompts.log.error(err.message) else if (err instanceof Error) prompts.log.error(err.message)
prompts.outro("Done") prompts.outro("Done")
+2 -1
View File
@@ -64,7 +64,7 @@ export namespace Config {
if (agentFolderPath.includes("/")) { if (agentFolderPath.includes("/")) {
const relativePath = agentFolderPath.replace(".md", "") const relativePath = agentFolderPath.replace(".md", "")
const pathParts = relativePath.split("/") const pathParts = relativePath.split("/")
agentName = pathParts.slice(0, -1).join("/").toUpperCase() + "/" + pathParts[pathParts.length - 1].toUpperCase() agentName = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1]
} }
const config = { const config = {
@@ -229,6 +229,7 @@ export namespace Config {
session_export: z.string().optional().default("<leader>x").describe("Export session to editor"), session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
session_new: z.string().optional().default("<leader>n").describe("Create a new session"), session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
session_list: z.string().optional().default("<leader>l").describe("List all sessions"), session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
session_share: z.string().optional().default("<leader>s").describe("Share current session"), session_share: z.string().optional().default("<leader>s").describe("Share current session"),
session_unshare: z.string().optional().default("none").describe("Unshare current session"), session_unshare: z.string().optional().default("none").describe("Unshare current session"),
session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"), session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"),
+1 -1
View File
@@ -28,7 +28,7 @@ await Promise.all([
fs.mkdir(Global.Path.bin, { recursive: true }), fs.mkdir(Global.Path.bin, { recursive: true }),
]) ])
const CACHE_VERSION = "8" const CACHE_VERSION = "9"
const version = await Bun.file(path.join(Global.Path.cache, "version")) const version = await Bun.file(path.join(Global.Path.cache, "version"))
.text() .text()
+2 -2
View File
@@ -25,8 +25,8 @@ export namespace Plugin {
} }
const plugins = [...(config.plugin ?? [])] const plugins = [...(config.plugin ?? [])]
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) { if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
plugins.push("opencode-copilot-auth") plugins.push("opencode-copilot-auth@0.0.2")
plugins.push("opencode-anthropic-auth") plugins.push("opencode-anthropic-auth@0.0.2")
} }
for (let plugin of plugins) { for (let plugin of plugins) {
log.info("loading plugin", { path: plugin }) log.info("loading plugin", { path: plugin })
@@ -36,6 +36,12 @@ export namespace Provider {
}, },
} }
}, },
async opencode() {
return {
autoload: true,
options: {},
}
},
openai: async () => { openai: async () => {
return { return {
autoload: false, autoload: false,
@@ -276,6 +282,15 @@ export namespace Provider {
} }
for (const [providerID, provider] of Object.entries(providers)) { for (const [providerID, provider] of Object.entries(providers)) {
// Filter out blacklisted models
const filteredModels = Object.fromEntries(
Object.entries(provider.info.models).filter(
([modelID]) =>
modelID !== "gpt-5-chat-latest" && !(providerID === "openrouter" && modelID === "openai/gpt-5-chat"),
),
)
provider.info.models = filteredModels
if (Object.keys(provider.info.models).length === 0) { if (Object.keys(provider.info.models).length === 0) {
delete providers[providerID] delete providers[providerID]
continue continue
+19
View File
@@ -726,6 +726,18 @@ export namespace Session {
synthetic: true, synthetic: true,
}) })
} }
const lastAssistantMsg = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
if (lastAssistantMsg?.mode === "plan" && agent.name === "build") {
msgs.at(-1)?.parts.push({
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
type: "text",
text: "You are now in build mode and are permitted to make edits",
synthetic: true,
})
}
let system = SystemPrompt.header(input.providerID) let system = SystemPrompt.header(input.providerID)
system.push( system.push(
...(() => { ...(() => {
@@ -945,6 +957,13 @@ export namespace Session {
toolName: "invalid", toolName: "invalid",
} }
}, },
headers:
input.providerID === "opencode"
? {
"x-opencode-session": input.sessionID,
"x-opencode-request": userMsg.id,
}
: undefined,
maxRetries: 3, maxRetries: 3,
activeTools: Object.keys(tools).filter((x) => x !== "invalid"), activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
maxOutputTokens: outputLimit, maxOutputTokens: outputLimit,
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "0.5.5", "version": "0.5.11",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
+18 -10
View File
@@ -32,12 +32,16 @@ export interface Hooks {
| { | {
method: "auto" method: "auto"
callback(): Promise< callback(): Promise<
| { | ({
type: "success" type: "success"
refresh: string } & (
access: string | {
expires: number refresh: string
} access: string
expires: number
}
| { key: string }
))
| { | {
type: "failed" type: "failed"
} }
@@ -46,12 +50,16 @@ export interface Hooks {
| { | {
method: "code" method: "code"
callback(code: string): Promise< callback(code: string): Promise<
| { | ({
type: "success" type: "success"
refresh: string } & (
access: string | {
expires: number refresh: string
} access: string
expires: number
}
| { key: string }
))
| { | {
type: "failed" type: "failed"
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "0.5.5", "version": "0.5.11",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
+7
View File
@@ -729,6 +729,9 @@ export type Config = {
} }
webfetch?: "ask" | "allow" | "deny" webfetch?: "ask" | "allow" | "deny"
} }
tools?: {
[key: string]: boolean
}
experimental?: { experimental?: {
hook?: { hook?: {
file_edited?: { file_edited?: {
@@ -794,6 +797,10 @@ export type KeybindsConfig = {
* List all sessions * List all sessions
*/ */
session_list: string session_list: string
/**
* Show session timeline
*/
session_timeline: string
/** /**
* Share current session * Share current session
*/ */
+22
View File
@@ -207,6 +207,9 @@ func New(
func (a *App) Keybind(commandName commands.CommandName) string { func (a *App) Keybind(commandName commands.CommandName) string {
command := a.Commands[commandName] command := a.Commands[commandName]
if len(command.Keybindings) == 0 {
return ""
}
kb := command.Keybindings[0] kb := command.Keybindings[0]
key := kb.Key key := kb.Key
if kb.RequiresLeader { if kb.RequiresLeader {
@@ -650,6 +653,25 @@ func (a *App) IsBusy() bool {
return false return false
} }
func (a *App) HasAnimatingWork() bool {
for _, msg := range a.Messages {
switch casted := msg.Info.(type) {
case opencode.AssistantMessage:
if casted.Time.Completed == 0 {
return true
}
}
for _, p := range msg.Parts {
if tp, ok := p.(opencode.ToolPart); ok {
if tp.State.Status == opencode.ToolPartStateStatusPending {
return true
}
}
}
}
return false
}
func (a *App) SaveState() tea.Cmd { func (a *App) SaveState() tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
err := SaveState(a.StatePath, a.State) err := SaveState(a.StatePath, a.State)
+35 -36
View File
@@ -107,7 +107,6 @@ func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command {
} }
const ( const (
SessionChildCycleCommand CommandName = "session_child_cycle" SessionChildCycleCommand CommandName = "session_child_cycle"
SessionChildCycleReverseCommand CommandName = "session_child_cycle_reverse" SessionChildCycleReverseCommand CommandName = "session_child_cycle_reverse"
ModelCycleRecentReverseCommand CommandName = "model_cycle_recent_reverse" ModelCycleRecentReverseCommand CommandName = "model_cycle_recent_reverse"
@@ -119,40 +118,40 @@ const (
EditorOpenCommand CommandName = "editor_open" EditorOpenCommand CommandName = "editor_open"
SessionNewCommand CommandName = "session_new" SessionNewCommand CommandName = "session_new"
SessionListCommand CommandName = "session_list" SessionListCommand CommandName = "session_list"
SessionNavigationCommand CommandName = "session_navigation" SessionTimelineCommand CommandName = "session_timeline"
SessionShareCommand CommandName = "session_share" SessionShareCommand CommandName = "session_share"
SessionUnshareCommand CommandName = "session_unshare" SessionUnshareCommand CommandName = "session_unshare"
SessionInterruptCommand CommandName = "session_interrupt" SessionInterruptCommand CommandName = "session_interrupt"
SessionCompactCommand CommandName = "session_compact" SessionCompactCommand CommandName = "session_compact"
SessionExportCommand CommandName = "session_export" SessionExportCommand CommandName = "session_export"
ToolDetailsCommand CommandName = "tool_details" ToolDetailsCommand CommandName = "tool_details"
ThinkingBlocksCommand CommandName = "thinking_blocks" ThinkingBlocksCommand CommandName = "thinking_blocks"
ModelListCommand CommandName = "model_list" ModelListCommand CommandName = "model_list"
AgentListCommand CommandName = "agent_list" AgentListCommand CommandName = "agent_list"
ModelCycleRecentCommand CommandName = "model_cycle_recent" ModelCycleRecentCommand CommandName = "model_cycle_recent"
ThemeListCommand CommandName = "theme_list" ThemeListCommand CommandName = "theme_list"
FileListCommand CommandName = "file_list" FileListCommand CommandName = "file_list"
FileCloseCommand CommandName = "file_close" FileCloseCommand CommandName = "file_close"
FileSearchCommand CommandName = "file_search" FileSearchCommand CommandName = "file_search"
FileDiffToggleCommand CommandName = "file_diff_toggle" FileDiffToggleCommand CommandName = "file_diff_toggle"
ProjectInitCommand CommandName = "project_init" ProjectInitCommand CommandName = "project_init"
InputClearCommand CommandName = "input_clear" InputClearCommand CommandName = "input_clear"
InputPasteCommand CommandName = "input_paste" InputPasteCommand CommandName = "input_paste"
InputSubmitCommand CommandName = "input_submit" InputSubmitCommand CommandName = "input_submit"
InputNewlineCommand CommandName = "input_newline" InputNewlineCommand CommandName = "input_newline"
MessagesPageUpCommand CommandName = "messages_page_up" MessagesPageUpCommand CommandName = "messages_page_up"
MessagesPageDownCommand CommandName = "messages_page_down" MessagesPageDownCommand CommandName = "messages_page_down"
MessagesHalfPageUpCommand CommandName = "messages_half_page_up" MessagesHalfPageUpCommand CommandName = "messages_half_page_up"
MessagesHalfPageDownCommand CommandName = "messages_half_page_down" MessagesHalfPageDownCommand CommandName = "messages_half_page_down"
MessagesPreviousCommand CommandName = "messages_previous" MessagesPreviousCommand CommandName = "messages_previous"
MessagesNextCommand CommandName = "messages_next" MessagesNextCommand CommandName = "messages_next"
MessagesFirstCommand CommandName = "messages_first" MessagesFirstCommand CommandName = "messages_first"
MessagesLastCommand CommandName = "messages_last" MessagesLastCommand CommandName = "messages_last"
MessagesLayoutToggleCommand CommandName = "messages_layout_toggle" MessagesLayoutToggleCommand CommandName = "messages_layout_toggle"
MessagesCopyCommand CommandName = "messages_copy" MessagesCopyCommand CommandName = "messages_copy"
MessagesUndoCommand CommandName = "messages_undo" MessagesUndoCommand CommandName = "messages_undo"
MessagesRedoCommand CommandName = "messages_redo" MessagesRedoCommand CommandName = "messages_redo"
AppExitCommand CommandName = "app_exit" AppExitCommand CommandName = "app_exit"
) )
func (k Command) Matches(msg tea.KeyPressMsg, leader bool) bool { func (k Command) Matches(msg tea.KeyPressMsg, leader bool) bool {
@@ -216,10 +215,10 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
Trigger: []string{"sessions", "resume", "continue"}, Trigger: []string{"sessions", "resume", "continue"},
}, },
{ {
Name: SessionNavigationCommand, Name: SessionTimelineCommand,
Description: "jump to message", Description: "show session timeline",
Keybindings: parseBindings("<leader>g"), Keybindings: parseBindings("<leader>g"),
Trigger: []string{"jump", "goto", "navigate"}, Trigger: []string{"timeline", "history", "goto"},
}, },
{ {
Name: SessionShareCommand, Name: SessionShareCommand,
@@ -339,6 +339,7 @@ func (m *editorComponent) Content() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
promptStyle := styles.NewStyle().Foreground(t.Primary()). promptStyle := styles.NewStyle().Foreground(t.Primary()).
Padding(0, 0, 0, 1). Padding(0, 0, 0, 1).
Bold(true) Bold(true)
@@ -213,6 +213,7 @@ func renderText(
extra string, extra string,
isThinking bool, isThinking bool,
isQueued bool, isQueued bool,
shimmer bool,
fileParts []opencode.FilePart, fileParts []opencode.FilePart,
agentParts []opencode.AgentPart, agentParts []opencode.AgentPart,
toolCalls ...opencode.ToolPart, toolCalls ...opencode.ToolPart,
@@ -234,7 +235,18 @@ func renderText(
} }
content = util.ToMarkdown(text, width, backgroundColor) content = util.ToMarkdown(text, width, backgroundColor)
if isThinking { if isThinking {
content = styles.NewStyle().Background(backgroundColor).Foreground(t.TextMuted()).Render("Thinking") + "\n\n" + content var label string
if shimmer {
label = util.Shimmer("Thinking...", backgroundColor, t.TextMuted(), t.Accent())
} else {
label = styles.NewStyle().Background(backgroundColor).Foreground(t.TextMuted()).Render("Thinking...")
}
label = styles.NewStyle().Background(backgroundColor).Width(width - 6).Render(label)
content = label + "\n\n" + content
} else if strings.TrimSpace(text) == "Generating..." {
label := util.Shimmer(text, backgroundColor, t.TextMuted(), t.Text())
label = styles.NewStyle().Background(backgroundColor).Width(width - 6).Render(label)
content = label
} }
case opencode.UserMessage: case opencode.UserMessage:
ts = time.UnixMilli(int64(casted.Time.Created)) ts = time.UnixMilli(int64(casted.Time.Created))
@@ -659,10 +671,22 @@ func renderToolDetails(
body = strings.Join(steps, "\n") body = strings.Join(steps, "\n")
body += "\n\n" body += "\n\n"
body += baseStyle(app.Keybind(commands.SessionChildCycleCommand)) +
mutedStyle(", ") + // Build navigation hint with proper spacing
baseStyle(app.Keybind(commands.SessionChildCycleReverseCommand)) + cycleKeybind := app.Keybind(commands.SessionChildCycleCommand)
mutedStyle(" navigate child sessions") cycleReverseKeybind := app.Keybind(commands.SessionChildCycleReverseCommand)
var navParts []string
if cycleKeybind != "" {
navParts = append(navParts, baseStyle(cycleKeybind))
}
if cycleReverseKeybind != "" {
navParts = append(navParts, baseStyle(cycleReverseKeybind))
}
if len(navParts) > 0 {
body += strings.Join(navParts, mutedStyle(", ")) + mutedStyle(" navigate child sessions")
}
} }
body = defaultStyle(body) body = defaultStyle(body)
default: default:
@@ -779,7 +803,9 @@ func renderToolTitle(
) string { ) string {
if toolCall.State.Status == opencode.ToolPartStateStatusPending { if toolCall.State.Status == opencode.ToolPartStateStatusPending {
title := renderToolAction(toolCall.Tool) title := renderToolAction(toolCall.Tool)
return styles.NewStyle().Width(width - 6).Render(title) t := theme.CurrentTheme()
shiny := util.Shimmer(title, t.BackgroundPanel(), t.TextMuted(), t.Accent())
return styles.NewStyle().Background(t.BackgroundPanel()).Width(width - 6).Render(shiny)
} }
toolArgs := "" toolArgs := ""
@@ -895,7 +921,9 @@ func renderArgs(args *map[string]any, titleKey string) string {
continue continue
} }
if key == "filePath" || key == "path" { if key == "filePath" || key == "path" {
value = util.Relative(value.(string)) if strValue, ok := value.(string); ok {
value = util.Relative(strValue)
}
} }
if key == titleKey { if key == titleKey {
title = fmt.Sprintf("%s", value) title = fmt.Sprintf("%s", value)
@@ -8,6 +8,7 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time"
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/lipgloss/v2"
@@ -59,6 +60,7 @@ type messagesComponent struct {
lineCount int lineCount int
selection *selection selection *selection
messagePositions map[string]int // map message ID to line position messagePositions map[string]int // map message ID to line position
animating bool
} }
type selection struct { type selection struct {
@@ -99,6 +101,7 @@ func (s selection) coords(offset int) *selection {
type ToggleToolDetailsMsg struct{} type ToggleToolDetailsMsg struct{}
type ToggleThinkingBlocksMsg struct{} type ToggleThinkingBlocksMsg struct{}
type shimmerTickMsg struct{}
func (m *messagesComponent) Init() tea.Cmd { func (m *messagesComponent) Init() tea.Cmd {
return tea.Batch(m.viewport.Init()) return tea.Batch(m.viewport.Init())
@@ -107,6 +110,15 @@ func (m *messagesComponent) Init() tea.Cmd {
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd var cmds []tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
case shimmerTickMsg:
if !m.app.HasAnimatingWork() {
m.animating = false
return m, nil
}
return m, tea.Sequence(
m.renderView(),
tea.Tick(90*time.Millisecond, func(t time.Time) tea.Msg { return shimmerTickMsg{} }),
)
case tea.MouseClickMsg: case tea.MouseClickMsg:
slog.Info("mouse", "x", msg.X, "y", msg.Y, "offset", m.viewport.YOffset) slog.Info("mouse", "x", msg.X, "y", msg.Y, "offset", m.viewport.YOffset)
y := msg.Y + m.viewport.YOffset y := msg.Y + m.viewport.YOffset
@@ -270,6 +282,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.dirty { if m.dirty {
cmds = append(cmds, m.renderView()) cmds = append(cmds, m.renderView())
} }
// Start shimmer ticks if any assistant/tool is in-flight
if !m.animating && m.app.HasAnimatingWork() {
m.animating = true
cmds = append(cmds, tea.Tick(90*time.Millisecond, func(t time.Time) tea.Msg { return shimmerTickMsg{} }))
}
} }
m.tail = m.viewport.AtBottom() m.tail = m.viewport.AtBottom()
@@ -318,6 +336,25 @@ func (m *messagesComponent) renderView() tea.Cmd {
width := m.width // always use full width width := m.width // always use full width
// Find the last streaming ReasoningPart to only shimmer that one
lastStreamingReasoningID := ""
if m.showThinkingBlocks {
for mi := len(m.app.Messages) - 1; mi >= 0 && lastStreamingReasoningID == ""; mi-- {
if _, ok := m.app.Messages[mi].Info.(opencode.AssistantMessage); !ok {
continue
}
parts := m.app.Messages[mi].Parts
for pi := len(parts) - 1; pi >= 0; pi-- {
if rp, ok := parts[pi].(opencode.ReasoningPart); ok {
if strings.TrimSpace(rp.Text) != "" && rp.Time.End == 0 {
lastStreamingReasoningID = rp.ID
break
}
}
}
}
}
reverted := false reverted := false
revertedMessageCount := 0 revertedMessageCount := 0
revertedToolCount := 0 revertedToolCount := 0
@@ -419,6 +456,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
files, files,
false, false,
isQueued, isQueued,
false,
fileParts, fileParts,
agentParts, agentParts,
) )
@@ -495,6 +533,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
"", "",
false, false,
false, false,
false,
[]opencode.FilePart{}, []opencode.FilePart{},
[]opencode.AgentPart{}, []opencode.AgentPart{},
toolCallParts..., toolCallParts...,
@@ -512,6 +551,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
"", "",
false, false,
false, false,
false,
[]opencode.FilePart{}, []opencode.FilePart{},
[]opencode.AgentPart{}, []opencode.AgentPart{},
toolCallParts..., toolCallParts...,
@@ -582,6 +622,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
} }
if part.Text != "" { if part.Text != "" {
text := part.Text text := part.Text
shimmer := part.Time.End == 0 && part.ID == lastStreamingReasoningID
content = renderText( content = renderText(
m.app, m.app,
message.Info, message.Info,
@@ -592,6 +633,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
"", "",
true, true,
false, false,
shimmer,
[]opencode.FilePart{}, []opencode.FilePart{},
[]opencode.AgentPart{}, []opencode.AgentPart{},
) )
@@ -626,6 +668,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
"", "",
false, false,
false, false,
false,
[]opencode.FilePart{}, []opencode.FilePart{},
[]opencode.AgentPart{}, []opencode.AgentPart{},
) )
@@ -728,16 +771,18 @@ func (m *messagesComponent) renderView() tea.Cmd {
} else { } else {
for _, part := range response.Parts { for _, part := range response.Parts {
if part.CallID == m.app.CurrentPermission.CallID { if part.CallID == m.app.CurrentPermission.CallID {
content := renderToolDetails( if toolPart, ok := part.AsUnion().(opencode.ToolPart); ok {
m.app, content := renderToolDetails(
part.AsUnion().(opencode.ToolPart), m.app,
m.app.CurrentPermission, toolPart,
width, m.app.CurrentPermission,
) width,
if content != "" { )
partCount++ if content != "" {
lineCount += lipgloss.Height(content) + 1 partCount++
blocks = append(blocks, content) lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
}
} }
} }
} }
@@ -18,8 +18,8 @@ import (
"github.com/sst/opencode/internal/util" "github.com/sst/opencode/internal/util"
) )
// NavigationDialog interface for the session navigation dialog // TimelineDialog interface for the session timeline dialog
type NavigationDialog interface { type TimelineDialog interface {
layout.Modal layout.Modal
} }
@@ -34,8 +34,8 @@ type RestoreToMessageMsg struct {
Index int Index int
} }
// navigationItem represents a user message in the navigation list // timelineItem represents a user message in the timeline list
type navigationItem struct { type timelineItem struct {
messageID string messageID string
content string content string
timestamp time.Time timestamp time.Time
@@ -43,25 +43,38 @@ type navigationItem struct {
toolCount int // Number of tools used in this message toolCount int // Number of tools used in this message
} }
func (n navigationItem) Render( func (n timelineItem) Render(
selected bool, selected bool,
width int, width int,
isFirstInViewport bool, isFirstInViewport bool,
baseStyle styles.Style, baseStyle styles.Style,
isCurrent bool,
) string { ) string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
infoStyle := baseStyle.Background(t.BackgroundPanel()).Foreground(t.Info()).Render infoStyle := baseStyle.Background(t.BackgroundPanel()).Foreground(t.Info()).Render
textStyle := baseStyle.Background(t.BackgroundPanel()).Foreground(t.Text()).Render textStyle := baseStyle.Background(t.BackgroundPanel()).Foreground(t.Text()).Render
// Add dot after timestamp if this is the current message - only apply color when not selected
var dot string
var dotVisualLen int
if isCurrent {
if selected {
dot = "● "
} else {
dot = lipgloss.NewStyle().Foreground(t.Success()).Render("● ")
}
dotVisualLen = 2 // "● " is 2 characters wide
}
// Format timestamp - only apply color when not selected // Format timestamp - only apply color when not selected
var timeStr string var timeStr string
var timeVisualLen int var timeVisualLen int
if selected { if selected {
timeStr = n.timestamp.Format("15:04") + " " timeStr = n.timestamp.Format("15:04") + " " + dot
timeVisualLen = lipgloss.Width(timeStr) timeVisualLen = lipgloss.Width(n.timestamp.Format("15:04")+" ") + dotVisualLen
} else { } else {
timeStr = infoStyle(n.timestamp.Format("15:04") + " ") timeStr = infoStyle(n.timestamp.Format("15:04")+" ") + dot
timeVisualLen = lipgloss.Width(timeStr) timeVisualLen = lipgloss.Width(n.timestamp.Format("15:04")+" ") + dotVisualLen
} }
// Tool count display (fixed width for alignment) - only apply color when not selected // Tool count display (fixed width for alignment) - only apply color when not selected
@@ -78,7 +91,7 @@ func (n navigationItem) Render(
} }
// Calculate available space for content // Calculate available space for content
// Reserve space for: timestamp + space + toolInfo + padding + some buffer // Reserve space for: timestamp + dot + space + toolInfo + padding + some buffer
reservedSpace := timeVisualLen + 1 + toolInfoVisualLen + 4 reservedSpace := timeVisualLen + 1 + toolInfoVisualLen + 4
contentWidth := max(width-reservedSpace, 8) contentWidth := max(width-reservedSpace, 8)
@@ -135,23 +148,23 @@ func (n navigationItem) Render(
return itemStyle.Render(text) return itemStyle.Render(text)
} }
func (n navigationItem) Selectable() bool { func (n timelineItem) Selectable() bool {
return true return true
} }
type navigationDialog struct { type timelineDialog struct {
width int width int
height int height int
modal *modal.Modal modal *modal.Modal
list list.List[navigationItem] list list.List[timelineItem]
app *app.App app *app.App
} }
func (n *navigationDialog) Init() tea.Cmd { func (n *timelineDialog) Init() tea.Cmd {
return nil return nil
} }
func (n *navigationDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (n *timelineDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
n.width = msg.Width n.width = msg.Width
@@ -163,7 +176,7 @@ func (n *navigationDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Handle navigation and immediately scroll to selected message // Handle navigation and immediately scroll to selected message
var cmd tea.Cmd var cmd tea.Cmd
listModel, cmd := n.list.Update(msg) listModel, cmd := n.list.Update(msg)
n.list = listModel.(list.List[navigationItem]) n.list = listModel.(list.List[timelineItem])
// Get the newly selected item and scroll to it immediately // Get the newly selected item and scroll to it immediately
if item, idx := n.list.GetSelectedItem(); idx >= 0 { if item, idx := n.list.GetSelectedItem(); idx >= 0 {
@@ -191,11 +204,11 @@ func (n *navigationDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
listModel, cmd := n.list.Update(msg) listModel, cmd := n.list.Update(msg)
n.list = listModel.(list.List[navigationItem]) n.list = listModel.(list.List[timelineItem])
return n, cmd return n, cmd
} }
func (n *navigationDialog) Render(background string) string { func (n *timelineDialog) Render(background string) string {
listView := n.list.View() listView := n.list.View()
t := theme.CurrentTheme() t := theme.CurrentTheme()
@@ -229,7 +242,7 @@ func (n *navigationDialog) Render(background string) string {
return n.modal.Render(content, background) return n.modal.Render(content, background)
} }
func (n *navigationDialog) Close() tea.Cmd { func (n *timelineDialog) Close() tea.Cmd {
return nil return nil
} }
@@ -268,9 +281,9 @@ func countToolsInResponse(messages []app.Message, userMessageIndex int) int {
return count return count
} }
// NewNavigationDialog creates a new session navigation dialog // NewTimelineDialog creates a new session timeline dialog
func NewNavigationDialog(app *app.App) NavigationDialog { func NewTimelineDialog(app *app.App) TimelineDialog { // renamed from NewNavigationDialog
var items []navigationItem var items []timelineItem
// Filter to only user messages and extract relevant info // Filter to only user messages and extract relevant info
for i, message := range app.Messages { for i, message := range app.Messages {
@@ -278,7 +291,7 @@ func NewNavigationDialog(app *app.App) NavigationDialog {
preview := extractMessagePreview(message.Parts) preview := extractMessagePreview(message.Parts)
toolCount := countToolsInResponse(app.Messages, i) toolCount := countToolsInResponse(app.Messages, i)
items = append(items, navigationItem{ items = append(items, timelineItem{
messageID: userMsg.ID, messageID: userMsg.ID,
content: preview, content: preview,
timestamp: time.UnixMilli(int64(userMsg.Time.Created)), timestamp: time.UnixMilli(int64(userMsg.Time.Created)),
@@ -290,25 +303,50 @@ func NewNavigationDialog(app *app.App) NavigationDialog {
listComponent := list.NewListComponent( listComponent := list.NewListComponent(
list.WithItems(items), list.WithItems(items),
list.WithMaxVisibleHeight[navigationItem](12), list.WithMaxVisibleHeight[timelineItem](12),
list.WithFallbackMessage[navigationItem]("No user messages in this session"), list.WithFallbackMessage[timelineItem]("No user messages in this session"),
list.WithAlphaNumericKeys[navigationItem](true), list.WithAlphaNumericKeys[timelineItem](true),
list.WithRenderFunc( list.WithRenderFunc(
func(item navigationItem, selected bool, width int, baseStyle styles.Style) string { func(item timelineItem, selected bool, width int, baseStyle styles.Style) string {
return item.Render(selected, width, false, baseStyle) // Determine if this item is the current message for the session
isCurrent := false
if app.Session.Revert.MessageID != "" {
// When reverted, Session.Revert.MessageID contains the NEXT user message ID
// So we need to find the previous user message to highlight the correct one
for i, navItem := range items {
if navItem.messageID == app.Session.Revert.MessageID && i > 0 {
// Found the next message, so the previous one is current
isCurrent = item.messageID == items[i-1].messageID
break
}
}
} else if len(app.Messages) > 0 {
// If not reverted, highlight the last user message
lastUserMsgID := ""
for i := len(app.Messages) - 1; i >= 0; i-- {
if userMsg, ok := app.Messages[i].Info.(opencode.UserMessage); ok {
lastUserMsgID = userMsg.ID
break
}
}
isCurrent = item.messageID == lastUserMsgID
}
// Only show the dot if undo/redo/restore is available
showDot := app.Session.Revert.MessageID != ""
return item.Render(selected, width, false, baseStyle, isCurrent && showDot)
}, },
), ),
list.WithSelectableFunc(func(item navigationItem) bool { list.WithSelectableFunc(func(item timelineItem) bool {
return true return true
}), }),
) )
listComponent.SetMaxWidth(layout.Current.Container.Width - 12) listComponent.SetMaxWidth(layout.Current.Container.Width - 12)
return &navigationDialog{ return &timelineDialog{
list: listComponent, list: listComponent,
app: app, app: app,
modal: modal.New( modal: modal.New(
modal.WithTitle("Jump to Message"), modal.WithTitle("Session Timeline"),
modal.WithMaxWidth(layout.Current.Container.Width-8), modal.WithMaxWidth(layout.Current.Container.Width-8),
), ),
} }
+4 -4
View File
@@ -728,8 +728,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "/tui/open-sessions": case "/tui/open-sessions":
sessionDialog := dialog.NewSessionDialog(a.app) sessionDialog := dialog.NewSessionDialog(a.app)
a.modal = sessionDialog a.modal = sessionDialog
case "/tui/open-navigation": case "/tui/open-timeline":
navigationDialog := dialog.NewNavigationDialog(a.app) navigationDialog := dialog.NewTimelineDialog(a.app)
a.modal = navigationDialog a.modal = navigationDialog
case "/tui/open-themes": case "/tui/open-themes":
themeDialog := dialog.NewThemeDialog() themeDialog := dialog.NewThemeDialog()
@@ -1146,11 +1146,11 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
case commands.SessionListCommand: case commands.SessionListCommand:
sessionDialog := dialog.NewSessionDialog(a.app) sessionDialog := dialog.NewSessionDialog(a.app)
a.modal = sessionDialog a.modal = sessionDialog
case commands.SessionNavigationCommand: case commands.SessionTimelineCommand:
if a.app.Session.ID == "" { if a.app.Session.ID == "" {
return a, toast.NewErrorToast("No active session") return a, toast.NewErrorToast("No active session")
} }
navigationDialog := dialog.NewNavigationDialog(a.app) navigationDialog := dialog.NewTimelineDialog(a.app)
a.modal = navigationDialog a.modal = navigationDialog
case commands.SessionShareCommand: case commands.SessionShareCommand:
if a.app.Session.ID == "" { if a.app.Session.ID == "" {
+21 -10
View File
@@ -2,12 +2,31 @@ package util
import ( import (
"context" "context"
"fmt"
"log/slog" "log/slog"
"reflect"
"sync" "sync"
opencode "github.com/sst/opencode-sdk-go" opencode "github.com/sst/opencode-sdk-go"
) )
func sanitizeValue(val any) any {
if val == nil {
return nil
}
if err, ok := val.(error); ok {
return err.Error()
}
v := reflect.ValueOf(val)
if v.Kind() == reflect.Interface && !v.IsNil() {
return fmt.Sprintf("%T", val)
}
return val
}
type APILogHandler struct { type APILogHandler struct {
client *opencode.Client client *opencode.Client
service string service string
@@ -67,21 +86,13 @@ func (h *APILogHandler) Handle(ctx context.Context, r slog.Record) error {
h.mu.Lock() h.mu.Lock()
for _, attr := range h.attrs { for _, attr := range h.attrs {
val := attr.Value.Any() val := attr.Value.Any()
if err, ok := val.(error); ok { extra[attr.Key] = sanitizeValue(val)
extra[attr.Key] = err.Error()
} else {
extra[attr.Key] = val
}
} }
h.mu.Unlock() h.mu.Unlock()
r.Attrs(func(attr slog.Attr) bool { r.Attrs(func(attr slog.Attr) bool {
val := attr.Value.Any() val := attr.Value.Any()
if err, ok := val.(error); ok { extra[attr.Key] = sanitizeValue(val)
extra[attr.Key] = err.Error()
} else {
extra[attr.Key] = val
}
return true return true
}) })
+143
View File
@@ -0,0 +1,143 @@
package util
import (
"math"
"os"
"strings"
"time"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/sst/opencode/internal/styles"
)
var shimmerStart = time.Now()
// Shimmer renders text with a moving foreground highlight.
// bg is the background color, dim is the base text color, bright is the highlight color.
func Shimmer(s string, bg compat.AdaptiveColor, _ compat.AdaptiveColor, _ compat.AdaptiveColor) string {
if s == "" {
return ""
}
runes := []rune(s)
n := len(runes)
if n == 0 {
return s
}
pad := 10
period := float64(n + pad*2)
sweep := 2.5
elapsed := time.Since(shimmerStart).Seconds()
pos := (math.Mod(elapsed, sweep) / sweep) * period
half := 4.0
type seg struct {
useHex bool
hex string
bold bool
faint bool
text string
}
var segs []seg
useHex := hasTrueColor()
for i, r := range runes {
ip := float64(i + pad)
dist := math.Abs(ip - pos)
t := 0.0
if dist <= half {
x := math.Pi * (dist / half)
t = 0.5 * (1.0 + math.Cos(x))
}
// Cosine brightness: base + amp*t (quantized for grouping)
base := 0.55
amp := 0.45
brightness := base
if t > 0 {
brightness = base + amp*t
}
lvl := int(math.Round(brightness * 255.0))
if !useHex {
step := 24 // ~11 steps across range for non-truecolor
lvl = int(math.Round(float64(lvl)/float64(step))) * step
}
bold := lvl >= 208
faint := lvl <= 128
// truecolor if possible; else fallback to modifiers only
hex := ""
if useHex {
if lvl < 0 {
lvl = 0
}
if lvl > 255 {
lvl = 255
}
hex = rgbHex(lvl, lvl, lvl)
}
if len(segs) == 0 {
segs = append(segs, seg{useHex: useHex, hex: hex, bold: bold, faint: faint, text: string(r)})
} else {
last := &segs[len(segs)-1]
if last.useHex == useHex && last.hex == hex && last.bold == bold && last.faint == faint {
last.text += string(r)
} else {
segs = append(segs, seg{useHex: useHex, hex: hex, bold: bold, faint: faint, text: string(r)})
}
}
}
var b strings.Builder
for _, g := range segs {
st := styles.NewStyle().Background(bg)
if g.useHex && g.hex != "" {
c := compat.AdaptiveColor{Dark: lipgloss.Color(g.hex), Light: lipgloss.Color(g.hex)}
st = st.Foreground(c)
}
if g.bold {
st = st.Bold(true)
}
if g.faint {
st = st.Faint(true)
}
b.WriteString(st.Render(g.text))
}
return b.String()
}
func hasTrueColor() bool {
c := strings.ToLower(os.Getenv("COLORTERM"))
return strings.Contains(c, "truecolor") || strings.Contains(c, "24bit")
}
func rgbHex(r, g, b int) string {
if r < 0 {
r = 0
}
if r > 255 {
r = 255
}
if g < 0 {
g = 0
}
if g > 255 {
g = 255
}
if b < 0 {
b = 0
}
if b > 255 {
b = 255
}
return "#" + hex2(r) + hex2(g) + hex2(b)
}
func hex2(v int) string {
const digits = "0123456789abcdef"
return string([]byte{digits[(v>>4)&0xF], digits[v&0xF]})
}
+7 -5
View File
@@ -9,8 +9,6 @@ import { rehypeHeadingIds } from "@astrojs/markdown-remark"
import rehypeAutolinkHeadings from "rehype-autolink-headings" import rehypeAutolinkHeadings from "rehype-autolink-headings"
import { spawnSync } from "child_process" import { spawnSync } from "child_process"
const github = "https://github.com/sst/opencode"
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: config.url, site: config.url,
@@ -49,7 +47,7 @@ export default defineConfig({
}, },
], ],
editLink: { editLink: {
baseUrl: `${github}/edit/dev/packages/web/`, baseUrl: `${config.github}/edit/dev/packages/web/`,
}, },
markdown: { markdown: {
headingLinks: false, headingLinks: false,
@@ -69,7 +67,7 @@ export default defineConfig({
{ {
label: "Usage", label: "Usage",
items: ["docs/cli", "docs/ide", "docs/share", "docs/github"], items: ["docs/tui", "docs/cli", "docs/ide", "docs/share", "docs/github"],
}, },
{ {
@@ -79,7 +77,6 @@ export default defineConfig({
"docs/agents", "docs/agents",
"docs/models", "docs/models",
"docs/themes", "docs/themes",
"docs/plugins",
"docs/keybinds", "docs/keybinds",
"docs/formatters", "docs/formatters",
"docs/permissions", "docs/permissions",
@@ -87,6 +84,11 @@ export default defineConfig({
"docs/mcp-servers", "docs/mcp-servers",
], ],
}, },
{
label: "Develop",
items: ["docs/sdk", "docs/server", "docs/plugins"],
},
], ],
components: { components: {
Hero: "./src/components/Hero.astro", Hero: "./src/components/Hero.astro",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@opencode/web", "name": "@opencode/web",
"type": "module", "type": "module",
"version": "0.5.5", "version": "0.5.11",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"dev:remote": "sst shell --stage=dev --target=Web astro dev", "dev:remote": "sst shell --stage=dev --target=Web astro dev",
@@ -179,8 +179,6 @@ Provide constructive feedback without making direct changes.
The markdown file name becomes the agent name. For example, `review.md` creates a `review` agent. The markdown file name becomes the agent name. For example, `review.md` creates a `review` agent.
Let's look at these configuration options in detail.
--- ---
## Options ## Options
+5 -118
View File
@@ -1,136 +1,23 @@
--- ---
title: CLI title: CLI
description: The opencode CLI options and commands. description: opencode CLI options and commands.
--- ---
import { Tabs, TabItem } from "@astrojs/starlight/components" import { Tabs, TabItem } from "@astrojs/starlight/components"
Running the opencode CLI starts it for the current directory. The opencode CLI by default starts the [TUI](/docs/tui) when run without any arguments.
```bash ```bash
opencode opencode
``` ```
Or you can start it for a specific working directory. But it also accepts commands as documented on this page. This allows you to interact with opencode programmatically.
```bash ```bash
opencode /path/to/project opencode run "Explain how closures work in JavaScript"
``` ```
---
## Slash commands
When using the opencode CLI, you can type `/` followed by a command name to quickly execute actions. For example:
:::tip
You can also use `@` to reference files in your messages.
:::
```bash frame="none"
/help
```
Here are all available slash commands:
- **`/help`**
Show the help dialog.
- **`/editor`**
Open external editor for composing messages. Uses the editor set in your `EDITOR` environment variable. [Learn more](#editor-setup).
- **`/export`**
Export current conversation to Markdown and open in your default editor. Uses the editor set in your `EDITOR` environment variable. [Learn more](#editor-setup).
- **`/new`**
Start a new session. _Alias_: `/clear`
- **`/sessions`**
List and switch between sessions. _Aliases_: `/resume`, `/continue`
- **`/share`**
Share current session. [Learn more](/docs/share).
- **`/unshare`**
Unshare current session. [Learn more](/docs/share#un-sharing).
- **`/compact`**
Compact the current session. _Alias_: `/summarize`
- **`/details`**
Toggle tool execution details.
- **`/models`**
List available models.
- **`/themes`**
List available themes.
- **`/init`**
Create or update `AGENTS.md` file. [Learn more](/docs/rules).
- **`/undo`**
Undo last message.
- **`/redo`**
Redo message.
- **`/exit`**
Exit opencode. _Aliases_: `/quit`, `/q`
---
### Bash commands
Start a line with `!` to run a shell command. The output is added to the conversation as a tool result.
```bash frame="none"
!ls -la
```
---
### Editor setup
Both the `/editor` and `/export` commands use the editor specified in your `EDITOR` environment variable.
<Tabs>
<TabItem label="Linux/macOS">
```bash export EDITOR=nano # or vim, code, etc. ``` To make it permanent, add this to your shell profile;
`~/.bashrc`, `~/.zshrc`, etc.
</TabItem>
<TabItem label="Windows (CMD)">
```bash set EDITOR=notepad # or code, vim, etc. ``` To make it permanent, use **System Properties** > **Environment
Variables**.
</TabItem>
<TabItem label="Windows (PowerShell)">
```bash $env:EDITOR = "notepad" # or "code", "vim", etc. ``` To make it permanent, add this to your PowerShell
profile.
</TabItem>
</Tabs>
Popular editor options include:
- `code` - Visual Studio Code
- `vim` - Vim editor
- `nano` - Nano editor
- `notepad` - Windows Notepad
- `subl` - Sublime Text
--- ---
@@ -289,7 +176,7 @@ opencode run Explain the use of context in Go
### serve ### serve
Start a headless opencode server for API access. Start a headless opencode server for API access. Check out the [server docs](/docs/server) for the full HTTP interface.
```bash ```bash
opencode serve opencode serve
+8 -8
View File
@@ -157,16 +157,16 @@ help.
You can ask opencode to explain the codebase to you. You can ask opencode to explain the codebase to you.
```txt frame="none" :::tip
Use the `@` key to fuzzy search for files in the project.
:::
```txt frame="none" "@packages/functions/src/api/index.ts"
How is authentication handled in @packages/functions/src/api/index.ts How is authentication handled in @packages/functions/src/api/index.ts
``` ```
This is helpful if there's a part of the codebase that you didn't work on. This is helpful if there's a part of the codebase that you didn't work on.
:::tip
Use the `@` key to fuzzy search for files in the project.
:::
--- ---
### Add features ### Add features
@@ -238,7 +238,7 @@ You can ask opencode to add new features to your project. Though we first recomm
For more straightforward changes, you can ask opencode to directly build it For more straightforward changes, you can ask opencode to directly build it
without having to review the plan first. without having to review the plan first.
```txt frame="none" ```txt frame="none" "@packages/functions/src/settings.ts" "@packages/functions/src/notes.ts"
We need to add authentication to the /settings route. Take a look at how this is We need to add authentication to the /settings route. Take a look at how this is
handled in the /notes route in @packages/functions/src/notes.ts and implement handled in the /notes route in @packages/functions/src/notes.ts and implement
the same logic in @packages/functions/src/settings.ts the same logic in @packages/functions/src/settings.ts
@@ -253,7 +253,7 @@ changes.
Let's say you ask opencode to make some changes. Let's say you ask opencode to make some changes.
```txt frame="none" ```txt frame="none" "@packages/functions/src/api/index.ts"
Can you refactor the function in @packages/functions/src/api/index.ts? Can you refactor the function in @packages/functions/src/api/index.ts?
``` ```
@@ -267,7 +267,7 @@ using the `/undo` command.
opencode will now revert the changes you made and show your original message opencode will now revert the changes you made and show your original message
again. again.
```txt frame="none" ```txt frame="none" "@packages/functions/src/api/index.ts"
Can you refactor the function in @packages/functions/src/api/index.ts? Can you refactor the function in @packages/functions/src/api/index.ts?
``` ```
@@ -20,7 +20,6 @@ opencode has a list of keybinds that you can customize through the opencode conf
"session_export": "<leader>x", "session_export": "<leader>x",
"session_new": "<leader>n", "session_new": "<leader>n",
"session_list": "<leader>l", "session_list": "<leader>l",
"session_navigation": "<leader>g",
"session_share": "<leader>s", "session_share": "<leader>s",
"session_unshare": "none", "session_unshare": "none",
"session_interrupt": "esc", "session_interrupt": "esc",
@@ -614,8 +614,6 @@ In this example:
### OpenAI ### OpenAI
https://platform.openai.com/api-keys
1. Head over to the [OpenAI Platform console](https://platform.openai.com/api-keys), click **Create new secret key**, and copy the key. 1. Head over to the [OpenAI Platform console](https://platform.openai.com/api-keys), click **Create new secret key**, and copy the key.
2. Run `opencode auth login` and select OpenAI. 2. Run `opencode auth login` and select OpenAI.
+300
View File
@@ -0,0 +1,300 @@
---
title: SDK
description: JS SDK for the opencode server.
---
import config from "../../../../config.mjs"
export const typesUrl = `${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts`
The opencode [JS/TS SDK](https://www.npmjs.com/package/@opencode-ai/sdk) provides a type-safe client for interacting with the opencode server. You can use it to build custom integrations and control opencode programmatically.
---
## Install
Install the SDK from npm:
```bash
npm install @opencode-ai/sdk
```
---
## Create client
Create a client instance to connect to your opencode server:
```javascript
import { createOpencodeClient } from "@opencode-ai/sdk"
const client = createOpencodeClient({
baseUrl: "http://localhost:4096",
})
```
#### Options
| Option | Type | Description | Default |
| --------- | ---------- | --------------------------- | ----------------------- |
| `baseUrl` | `string` | URL of the opencode server | `http://localhost:4096` |
| `fetch` | `function` | Custom fetch implementation | `globalThis.fetch` |
---
## Start server
You can also programmatically start an opencode server:
```javascript
import { createOpencodeServer } from "@opencode-ai/sdk"
const server = await createOpencodeServer({
host: "127.0.0.1",
port: 4096,
})
console.log(`Server running at ${server.url}`)
server.close()
```
---
## Types
The SDK includes TypeScript definitions for all API types. Import them directly:
```typescript
import type { Session, Message, Part } from "@opencode-ai/sdk"
```
All types are generated from the server's OpenAPI specification and available in the <a href={typesUrl}>types file</a>.
---
## APIs
The SDK exposes all server APIs through a type-safe client interface.
---
### App
| Method | Description | Response |
| ------------ | ------------------ | --------------------------------------- |
| `app.get()` | Get app info | <a href={typesUrl}><code>App</code></a> |
| `app.init()` | Initialize the app | `boolean` |
---
#### Examples
```javascript
const app = await client.app.get()
await client.app.init()
```
---
### Config
| Method | Description | Response |
| -------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `config.get()` | Get config info | <a href={typesUrl}><code>Config</code></a> |
| `config.providers()` | List providers and default models | `{ providers: `<a href={typesUrl}><code>Provider[]</code></a>`, default: { [key: string]: string } }` |
---
#### Examples
```javascript
const config = await client.config.get()
const { providers, default: defaults } = await client.config.providers()
```
---
### Sessions
| Method | Description | Notes |
| ------------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `session.list()` | List sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.get({ id })` | Get session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.children({ id })` | List child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.create({ parentID?, title? })` | Create session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.delete({ id })` | Delete session | Returns `boolean` |
| `session.update({ id, title? })` | Update session properties | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.init({ id, messageID, providerID, modelID })` | Analyze app and create `AGENTS.md` | Returns `boolean` |
| `session.abort({ id })` | Abort a running session | Returns `boolean` |
| `session.share({ id })` | Share session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unshare({ id })` | Unshare session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.summarize({ id, providerID, modelID })` | Summarize session | Returns `boolean` |
| `session.messages({ id })` | List messages in a session | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` |
| `session.message({ id, messageID })` | Get message details | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.chat({ id, ...chatInput })` | Send chat message | Returns <a href={typesUrl}><code>Message</code></a> |
| `session.shell({ id, agent, command })` | Run a shell command | Returns <a href={typesUrl}><code>Message</code></a> |
| `session.revert({ id, messageID, partID? })` | Revert a message | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unrevert({ id })` | Restore reverted messages | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.permissions.respond({ id, permissionID, response })` | Respond to a permission request | Returns `boolean` |
---
#### Examples
```javascript
// Create and manage sessions
const session = await client.session.create({ title: "My session" })
const sessions = await client.session.list()
// Send messages
const message = await client.session.chat({
id: session.id,
providerID: "anthropic",
modelID: "claude-3-5-sonnet-20241022",
parts: [{ type: "text", text: "Hello!" }],
})
```
---
### Files
| Method | Description | Response |
| ------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------- |
| `find.text({ pattern })` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `find.files({ query })` | Find files by name | `string[]` (file paths) |
| `find.symbols({ query })` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
| `file.read({ path })` | Read a file | `{ type: "raw" \| "patch", content: string }` |
| `file.status()` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |
---
#### Examples
```javascript
// Search and read files
const textResults = await client.find.text({ pattern: "function.*opencode" })
const files = await client.find.files({ query: "*.ts" })
const content = await client.file.read({ path: "src/index.ts" })
```
---
### Logging
| Method | Description | Response |
| ------------------------------------------------ | --------------- | --------- |
| `log.write({ service, level, message, extra? })` | Write log entry | `boolean` |
---
#### Examples
```javascript
await client.log.write({
service: "my-app",
level: "info",
message: "Operation completed",
})
```
---
### Agents
| Method | Description | Response |
| -------------- | ------------------------- | ------------------------------------------- |
| `agent.list()` | List all available agents | <a href={typesUrl}><code>Agent[]</code></a> |
---
#### Examples
```javascript
const agents = await client.agent.list()
```
---
### TUI
| Method | Description | Response |
| --------------------------------------------- | --------------------------------- | ---------------------- |
| `tui.appendPrompt({ text })` | Append text to the prompt | `boolean` |
| `tui.openHelp()` | Open the help dialog | `boolean` |
| `tui.openSessions()` | Open the session selector | `boolean` |
| `tui.openThemes()` | Open the theme selector | `boolean` |
| `tui.openModels()` | Open the model selector | `boolean` |
| `tui.submitPrompt()` | Submit the current prompt | `boolean` |
| `tui.clearPrompt()` | Clear the prompt | `boolean` |
| `tui.executeCommand({ command })` | Execute a command | `boolean` |
| `tui.showToast({ title?, message, variant })` | Show toast notification | `boolean` |
| `tui.control.next()` | Wait for the next control request | Control request object |
| `tui.control.response({ body })` | Respond to a control request | `boolean` |
---
#### Examples
```javascript
// Control TUI interface
await client.tui.appendPrompt({ text: "Add this to prompt" })
await client.tui.showToast({
message: "Task completed",
variant: "success",
})
```
---
### Auth
| Method | Description | Response |
| ------------------------------- | ------------------------------ | --------- |
| `auth.set({ id, ...authData })` | Set authentication credentials | `boolean` |
---
#### Examples
```javascript
await client.auth.set({
id: "anthropic",
type: "api",
key: "your-api-key",
})
```
---
### Events
| Method | Description | Response |
| ------------------- | ------------------------- | ------------------------- |
| `event.subscribe()` | Server-sent events stream | Server-sent events stream |
---
#### Examples
```javascript
// Listen to real-time events
const eventStream = await client.event.subscribe()
for await (const event of eventStream) {
console.log("Event:", event.type, event.properties)
}
```
---
## Error handling
The SDK throws typed errors that you can catch and handle:
```typescript
try {
const session = await client.session.get({ id: "invalid-id" })
} catch (error) {
console.error("Failed to get session:", error.message)
}
```
@@ -0,0 +1,155 @@
---
title: Server
description: Interact with opencode server over HTTP.
---
import config from "../../../../config.mjs"
export const typesUrl = `${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts`
The `opencode serve` command runs a headless HTTP server that exposes an API that an opencode client, like the TUI, uses. You can use this to create your own opencode client and even control a currently running TUI.
---
## Usage
```bash
opencode serve [--port <number>] [--hostname <string>]
```
#### Options
| Flag | Short | Description | Default |
| ------------ | ----- | --------------------- | ----------- |
| `--port` | `-p` | Port to listen on | `4096` |
| `--hostname` | `-h` | Hostname to listen on | `127.0.0.1` |
---
## Docs
The server publishes an OpenAPI 3.1 spec that can be viewed at:
```
http://<hostname>:<port>/doc
```
For example, `http://localhost:4096/doc`. Use the spec to generate clients or inspect request and response types. Or view it in a Swagger explorer.
---
## APIs
The opencode server exposes the following APIs.
---
### App
| Method | Path | Description | Response |
| ------ | ----------- | ------------------ | --------------------------------------- |
| `GET` | `/app` | Get app info | <a href={typesUrl}><code>App</code></a> |
| `POST` | `/app/init` | Initialize the app | `boolean` |
---
### Config
| Method | Path | Description | Response |
| ------ | ------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `GET` | `/config` | Get config info | <a href={typesUrl}><code>Config</code></a> |
| `GET` | `/config/providers` | List providers and default models | `{ providers: `<a href={typesUrl}>Provider[]</a>`, default: { [key: string]: string } }` |
---
### Sessions
| Method | Path | Description | Notes |
| -------- | ---------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/session` | List sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `GET` | `/session/:id` | Get session | Returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/:id/children` | List child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `POST` | `/session` | Create session | body: `{ parentID?, title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id` | Delete session | |
| `PATCH` | `/session/:id` | Update session properties | body: `{ title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `POST` | `/session/:id/init` | Analyze app and create `AGENTS.md` | body: `{ messageID, providerID, modelID }` |
| `POST` | `/session/:id/abort` | Abort a running session | |
| `POST` | `/session/:id/share` | Share session | Returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id/share` | Unshare session | Returns <a href={typesUrl}><code>Session</code></a> |
| `POST` | `/session/:id/summarize` | Summarize session | |
| `GET` | `/session/:id/message` | List messages in a session | Returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}[]` |
| `GET` | `/session/:id/message/:messageID` | Get message details | Returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `POST` | `/session/:id/message` | Send chat message | body matches [`ChatInput`](https://github.com/sst/opencode/blob/main/packages/opencode/src/session/index.ts#L358), returns <a href={typesUrl}><code>Message</code></a> |
| `POST` | `/session/:id/shell` | Run a shell command | body matches [`CommandInput`](https://github.com/sst/opencode/blob/main/packages/opencode/src/session/index.ts#L1007), returns <a href={typesUrl}><code>Message</code></a> |
| `POST` | `/session/:id/revert` | Revert a message | body: `{ messageID }` |
| `POST` | `/session/:id/unrevert` | Restore reverted messages | |
| `POST` | `/session/:id/permissions/:permissionID` | Respond to a permission request | body: `{ response }` |
---
### Files
| Method | Path | Description | Response |
| ------ | ------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------- |
| `GET` | `/find?pattern=<pat>` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `GET` | `/find/file?query=<q>` | Find files by name | `string[]` (file paths) |
| `GET` | `/find/symbol?query=<q>` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
| `GET` | `/file?path=<path>` | Read a file | `{ type: "raw" \| "patch", content: string }` |
| `GET` | `/file/status` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |
---
### Logging
| Method | Path | Description | Response |
| ------ | ------ | ------------------------------------------------------------ | --------- |
| `POST` | `/log` | Write log entry. Body: `{ service, level, message, extra? }` | `boolean` |
---
### Agents
| Method | Path | Description | Response |
| ------ | -------- | ------------------------- | ------------------------------------------- |
| `GET` | `/agent` | List all available agents | <a href={typesUrl}><code>Agent[]</code></a> |
---
### TUI
| Method | Path | Description | Response |
| ------ | ----------------------- | ------------------------------------------- | ---------------------- |
| `POST` | `/tui/append-prompt` | Append text to the prompt | `boolean` |
| `POST` | `/tui/open-help` | Open the help dialog | `boolean` |
| `POST` | `/tui/open-sessions` | Open the session selector | `boolean` |
| `POST` | `/tui/open-themes` | Open the theme selector | `boolean` |
| `POST` | `/tui/open-models` | Open the model selector | `boolean` |
| `POST` | `/tui/submit-prompt` | Submit the current prompt | `boolean` |
| `POST` | `/tui/clear-prompt` | Clear the prompt | `boolean` |
| `POST` | `/tui/execute-command` | Execute a command (`{ command }`) | `boolean` |
| `POST` | `/tui/show-toast` | Show toast (`{ title?, message, variant }`) | `boolean` |
| `GET` | `/tui/control/next` | Wait for the next control request | Control request object |
| `POST` | `/tui/control/response` | Respond to a control request (`{ body }`) | `boolean` |
---
### Auth
| Method | Path | Description | Response |
| ------ | ----------- | --------------------------------------------------------------- | --------- |
| `PUT` | `/auth/:id` | Set authentication credentials. Body must match provider schema | `boolean` |
---
### Events
| Method | Path | Description | Response |
| ------ | -------- | ----------------------------------------------------------------------------- | ------------------------- |
| `GET` | `/event` | Server-sent events stream. First event is `server.connected`, then bus events | Server-sent events stream |
---
### Docs
| Method | Path | Description | Response |
| ------ | ------ | -------------------------------------- | ------------------------------------------ |
| `GET` | `/doc` | OpenAPI 3.1 specification | HTML page with OpenAPI spec |
+290
View File
@@ -0,0 +1,290 @@
---
title: TUI
description: Using the opencode terminal user interface.
---
import { Tabs, TabItem } from "@astrojs/starlight/components"
opencode provides an interactive terminal interface or TUI for working on your projects with an LLM.
Running opencode starts the TUI for the current directory.
```bash
opencode
```
Or you can start it for a specific working directory.
```bash
opencode /path/to/project
```
Once you're in the TUI, you can prompt it with a message.
```text
Give me a quick summary of the codebase.
```
:::tip
You can also use `@` to reference files in your messages.
:::
```text "@packages/functions/src/api/index.ts"
How is auth handled in @packages/functions/src/api/index.ts?
```
---
## Commands
When using the opencode TUI, you can type `/` followed by a command name to quickly execute actions. For example:
```bash frame="none"
/help
```
Most commands also have keybind using `ctrl+x` as the leader key, where `ctrl+x` is the default leader key. [Learn more](/docs/keybinds).
Here are all available slash commands:
---
### compact
Compact the current session. _Alias_: `/summarize`
```bash frame="none"
/compact
```
**Keybind:** `ctrl+x c`
---
### details
Toggle tool execution details.
```bash frame="none"
/details
```
**Keybind:** `ctrl+x d`
---
### editor
Open external editor for composing messages. Uses the editor set in your `EDITOR` environment variable. [Learn more](#editor-setup).
```bash frame="none"
/editor
```
**Keybind:** `ctrl+x e`
---
### exit
Exit opencode. _Aliases_: `/quit`, `/q`
```bash frame="none"
/exit
```
**Keybind:** `ctrl+x q`
---
### export
Export current conversation to Markdown and open in your default editor. Uses the editor set in your `EDITOR` environment variable. [Learn more](#editor-setup).
```bash frame="none"
/export
```
**Keybind:** `ctrl+x x`
---
### help
Show the help dialog.
```bash frame="none"
/help
```
**Keybind:** `ctrl+x h`
---
### init
Create or update `AGENTS.md` file. [Learn more](/docs/rules).
```bash frame="none"
/init
```
**Keybind:** `ctrl+x i`
---
### models
List available models.
```bash frame="none"
/models
```
**Keybind:** `ctrl+x m`
---
### new
Start a new session. _Alias_: `/clear`
```bash frame="none"
/new
```
**Keybind:** `ctrl+x n`
---
### redo
Redo a previously undone message. Only available after using `/undo`.
:::tip
Any file changes will also be restored.
:::
```bash frame="none"
/redo
```
**Keybind:** `ctrl+x r`
---
### sessions
List and switch between sessions. _Aliases_: `/resume`, `/continue`
```bash frame="none"
/sessions
```
**Keybind:** `ctrl+x l`
---
### share
Share current session. [Learn more](/docs/share).
```bash frame="none"
/share
```
**Keybind:** `ctrl+x s`
---
### themes
List available themes.
```bash frame="none"
/themes
```
**Keybind:** `ctrl+x t`
---
### undo
Undo last message in the conversation. Removes the most recent user message, all subsequent responses, and any file changes.
:::tip
Any file changes made will also be reverted.
:::
```bash frame="none"
/undo
```
**Keybind:** `ctrl+x u`
---
### unshare
Unshare current session. [Learn more](/docs/share#un-sharing).
```bash frame="none"
/unshare
```
---
## Bash commands
Start a message with `!` to run a shell command.
```bash frame="none"
!ls -la
```
The output of the command is added to the conversation as a tool result.
---
## Editor setup
Both the `/editor` and `/export` commands use the editor specified in your `EDITOR` environment variable.
<Tabs>
<TabItem label="Linux/macOS">
```bash
export EDITOR=nano # or vim, code, etc.
```
To make it permanent, add this to your shell profile;
`~/.bashrc`, `~/.zshrc`, etc.
</TabItem>
<TabItem label="Windows (CMD)">
```bash
set EDITOR=notepad # or code, vim, etc.
```
To make it permanent, use **System Properties** > **Environment
Variables**.
</TabItem>
<TabItem label="Windows (PowerShell)">
```bash
$env:EDITOR = "notepad" # or "code", "vim", etc.
```
To make it permanent, add this to your PowerShell
profile.
</TabItem>
</Tabs>
Popular editor options include:
- `code` - Visual Studio Code
- `vim` - Vim editor
- `nano` - Nano editor
- `notepad` - Windows Notepad
- `subl` - Sublime Text
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode", "name": "opencode",
"displayName": "opencode", "displayName": "opencode",
"description": "opencode for VS Code", "description": "opencode for VS Code",
"version": "0.5.5", "version": "0.5.11",
"publisher": "sst-dev", "publisher": "sst-dev",
"repository": { "repository": {
"type": "git", "type": "git",