Compare commits

..
76 Commits
Author SHA1 Message Date
opencode c47fde2ca4 release: v1.0.14 2025-11-03 00:12:08 +00:00
Dax Raad f42e1c6375 tui: fix focus management and dialog interactions 2025-11-02 19:07:22 -05:00
Dax Raad f68374ad22 DELETE GO BUBBLETEA CRAP HOORAY 2025-11-02 18:43:33 -05:00
opencode 5e86c9b791 release: v1.0.13 2025-11-02 23:31:25 +00:00
Dax Raad 94658c31c5 add back child session cycle 2025-11-02 18:26:38 -05:00
Dax Raad 9fd672a1cb undo 2025-11-02 16:31:32 -05:00
Dax Raad 10523c4372 move dialog select keybind to input 2025-11-02 15:47:04 -05:00
Dax Raad d1cd7d0344 ci: centralize Bun version to package.json to ensure consistent builds across CI and local development 2025-11-02 15:42:15 -05:00
Dax Raad 06ac1be226 upgrade to bun 1.3.1 2025-11-02 14:00:50 -05:00
Dax Raad 05489bc843 tui: fix file path handling when pasting images with spaces in filename
- Fixes issue where files with spaces in their names couldn't be pasted as images
- Prevents default paste behavior to avoid conflicts with image insertion
- Improves error handling for file reading operations
2025-11-02 13:45:44 -05:00
Dax Raad 3f02eecf22 tui: add /timeline command to quickly navigate to specific messages in session history 2025-11-02 18:27:42 +00:00
opencode f5ca78ed7b release: v1.0.12 2025-11-02 18:27:42 +00:00
Dax Raad 894cbaa51e fix duplicate plugin subscriptions 2025-11-02 13:22:58 -05:00
John Eismeier 8b70b89fde fix: typos (#3757)
Signed-off-by: John E <jeis4wpi@outlook.com>
2025-11-02 09:56:40 -06:00
Aditya Mathur f9dbc586dc chore: update hono-openapi to version 1.1.1 (#3738) 2025-11-02 09:21:55 -06:00
GitHub Action ffeef63ca1 ignore: update download stats 2025-11-02 2025-11-02 12:04:05 +00:00
kcrommett 4da58294d9 add nightowl theme back after opentui release (#3732) 2025-11-02 04:29:14 -05:00
opencode fa2e88f49b release: v1.0.11 2025-11-02 08:18:59 +00:00
Dax Raad 28e765ef0a fix dialog 2025-11-02 02:53:55 -05:00
Dax Raad bfbcb5f200 tui: prevent default Enter key behavior when selecting dialog options to avoid conflicts 2025-11-02 01:19:30 -05:00
Aiden Cline 89492b3002 ci: fix regex 2025-11-01 20:23:10 -05:00
opencode-agent[bot]opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>rekram1-node
2663415d47 github action: truncate PR titles to 256 chars to avoid GH api errors (#3727)
publish-github-action / publish (push) Has been cancelled
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-01 20:09:35 -05:00
Aiden Cline 51be67cc14 ci: stop auto assigning 2025-11-01 19:57:09 -05:00
Sebastian Herrlinger 92a1943771 upgrade to opentui 0.1.32, activates kitty keyboard 2025-11-02 01:45:38 +01:00
opencode 1e15fc273a release: v1.0.10 2025-11-01 18:06:28 +00:00
Dax 104a895a71 Light mode (#3709) 2025-11-01 13:54:01 -04:00
Dax Raad f98e730405 docs update 2025-11-01 13:23:03 -04:00
Dax Raad b12bef05d3 docs: update keybinds documentation with current defaults and remove deprecated bindings 2025-11-01 12:32:22 -04:00
opencode 2f1d001cc5 release: v1.0.9 2025-11-01 16:22:49 +00:00
Dax Raad 65d0b3ed6d sync 2025-11-01 12:14:15 -04:00
Haris Gušić 22a34d7958 feat: tui: Port /exit command and all command aliases (#3665) 2025-11-01 12:13:10 -04:00
Aiden Cline cb4401ec92 ignore: update contributing md 2025-11-01 11:08:07 -05:00
opencode febf467b03 release: v1.0.8 2025-11-01 15:58:23 +00:00
Dax Raad d55a2fd56c tui: change delete keybind to ctrl+d in session list dialog 2025-11-01 11:53:46 -04:00
Dax Raad 40f577e5e7 fix modified files being empty 2025-11-01 11:48:47 -04:00
Dax Raad 9e49870118 remember sidebar position 2025-11-01 11:40:33 -04:00
Daniel van Strienandcélina fe38e3ab02 docs: add Hugging Face Inference Providers documentation (#3505)
Co-authored-by: célina <hanouticelina@gmail.com>
2025-11-01 10:33:17 -05:00
Haris Gušić 0170577743 feat: tui: Add --prompt option (#3668) 2025-11-01 11:18:31 -04:00
Giuseppe Rota 7de6ea5922 fix: fix typo in commit message guidelines (#3702) 2025-11-01 10:14:53 -05:00
Yuku Kotani 2fe7d13e69 Add formatter status display to TUI status dialog (#3701) 2025-11-01 11:14:39 -04:00
Dax Raad 1bc3c98ae7 ensure wl-copy is available 2025-11-01 11:10:39 -04:00
Haris Gušić 55787f2caa fix: tui: Handle Clipboard.copy errors properly (#3685) 2025-11-01 15:34:21 +01:00
Haris Gušić 7df61a74a0 fix: tui: add toast for /share url copy (#3686) 2025-11-01 08:06:56 -05:00
GitHub Action 4f23110880 ignore: update download stats 2025-11-01 2025-11-01 12:04:18 +00:00
Aiden Cline 041353f4ff make /init a default slash command on server side (#3677) 2025-11-01 01:14:09 -05:00
Haris Gušić c72f8b17c6 fix: tui: Fix /editor command (#3663) 2025-11-01 00:16:06 +00:00
opencode eb304f4115 release: v1.0.7 2025-11-01 00:16:05 +00:00
Dax Raad 5565f14ef5 tab to accept autocomplete 2025-10-31 20:10:01 -04:00
Dax Raad 10a4455c6f tui: fix prompt text aggregation to exclude synthetic content 2025-10-31 20:01:27 -04:00
Dax Raad 5ded6d6ad7 docs: sync 2025-10-31 23:58:57 +00:00
opencode 849a38c30c release: v1.0.6 2025-10-31 23:58:57 +00:00
Dax Raad 68050ab802 tui: prevent clipboard operations from throwing errors on process exit 2025-10-31 19:54:15 -04:00
opencode 91d01fd4cc release: v1.0.5 2025-10-31 23:51:36 +00:00
Dax Raad 9beb0f8512 tui: improve keyboard navigation and MCP server status display 2025-10-31 19:47:08 -04:00
Dax Raad d4cb47eadc tui: add keyboard shortcuts to cycle through recently used models
Users can now press F2 to cycle forward and Shift+F2 to cycle backward through their recently used models, making it faster to switch between commonly used AI models without opening the model selection dialog.
2025-10-31 19:42:41 -04:00
Dax Raad 261ff416a9 sync 2025-10-31 23:05:11 +00:00
opencode d0a70cb217 release: v1.0.4 2025-10-31 23:05:10 +00:00
Aiden Cline 20fc56d020 Revert "opentui: fix: Make worker.ts path independent from cwd (#3600)"
This reverts commit d473d4ffc8.
2025-10-31 17:57:56 -05:00
opencode a57ae3ec93 release: v1.0.3 2025-10-31 22:52:57 +00:00
Dax Raad 30f9fa12d9 tui: add session rename functionality with /rename command
- Add /rename command to autocomplete when a session is active
- Add rename dialog component for changing session names
- Add rename option to session list dialog with 'r' keybind
- Add session rename command to command registry
2025-10-31 18:44:33 -04:00
Haris Gušić d473d4ffc8 opentui: fix: Make worker.ts path independent from cwd (#3600) 2025-10-31 17:37:31 -05:00
Haris Gušić af50596529 fix: grep failing when pattern started with a dash 2025-10-31 17:20:22 -05:00
Dax Raad 3823d8d50e tui: simplify theme selection API by renaming setSelectedTheme to set 2025-10-31 18:11:36 -04:00
Dax Raad 7a926b32ce respect theme in config 2025-10-31 18:04:38 -04:00
Haris Gušić a5ede68241 fix: Remove conflicting "-h" aliases in TUI spawn and thread commands (#3651) 2025-10-31 16:59:59 -05:00
Aiden Cline 60dc38050d fix: unsupported option 2025-10-31 16:53:08 -05:00
Dax Raad 31d0caee38 tui: add /editor command to autocomplete for opening external editor 2025-10-31 17:47:08 -04:00
Dax Raad 2a7ab45605 add /theme 2025-10-31 17:44:41 -04:00
Aiden Cline 019054dd1e Revert "fix: ensure flags & docs match (#3638)"
This reverts commit a018a15f32.
2025-10-31 16:43:29 -05:00
opencode-agent[bot]opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>rekram1-node
a018a15f32 fix: ensure flags & docs match (#3638)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-10-31 16:33:46 -05:00
Nathan Thomas e630d680dd feat: allow ctrl+d to exit the app (#3636) 2025-10-31 16:27:41 -05:00
Haris Gušić 9e392f25a6 feat: Improve error boundary add button to easily create issue in github (#3639) 2025-10-31 16:20:32 -05:00
Aiden Cline 2cc4e6ad7c ci: change model 2025-10-31 16:06:22 -05:00
Adam 70d8d1ab1e wip: desktop work 2025-10-31 15:57:21 -05:00
Adam 342aa27e03 wip: desktop work 2025-10-31 15:37:50 -05:00
Adam e1aed0cd01 wip: desktop work 2025-10-31 15:37:50 -05:00
213 changed files with 2209 additions and 29749 deletions
+2
View File
@@ -5,6 +5,8 @@ runs:
steps: steps:
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- name: Cache ~/.bun - name: Cache ~/.bun
id: cache-bun id: cache-bun
+1 -8
View File
@@ -21,7 +21,7 @@ jobs:
const description = issue.body || ''; const description = issue.body || '';
// Check for version patterns like v1.0.x or 1.0.x // Check for version patterns like v1.0.x or 1.0.x
const versionPattern = /\b[v]?1\.0\.[x\d]\b/i; const versionPattern = /[v]?1\.0\./i;
if (versionPattern.test(title) || versionPattern.test(description)) { if (versionPattern.test(title) || versionPattern.test(description)) {
await github.rest.issues.addLabels({ await github.rest.issues.addLabels({
@@ -30,11 +30,4 @@ jobs:
issue_number: issue.number, issue_number: issue.number,
labels: ['opentui'] labels: ['opentui']
}); });
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
assignees: ['thdxr']
});
} }
+1 -1
View File
@@ -26,4 +26,4 @@ jobs:
env: env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
with: with:
model: opencode/kimi-k2 model: opencode/glm-4.6
+1
View File
@@ -5,6 +5,7 @@ node_modules
.env .env
.idea .idea
.vscode .vscode
*~
openapi.json openapi.json
playground playground
tmp tmp
+1 -1
View File
@@ -19,5 +19,5 @@ For anything in the packages/app use the ignore: prefix.
prefer to explain WHY something was done from an end user perspective instead of prefer to explain WHY something was done from an end user perspective instead of
WHAT was done. WHAT was done.
do not do generic messages like "improvied agent experience" be very specific do not do generic messages like "improved agent experience" be very specific
about what user facing changes were made about what user facing changes were made
+1 -1
View File
@@ -17,7 +17,7 @@
## Tool Calling ## Tool Calling
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. Here is an example illustrating how to execute 3 parallel file reads in this chat environnement: - ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. Here is an example illustrating how to execute 3 parallel file reads in this chat environment:
json json
{ {
+3 -3
View File
@@ -26,7 +26,7 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
## Developing OpenCode ## Developing OpenCode
- Requirements: Bun 1.3+, Go 1.24.x. - Requirements: Bun 1.3+
- Install dependencies and start the dev server from the repo root: - Install dependencies and start the dev server from the repo root:
```bash ```bash
@@ -36,11 +36,11 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
- Core pieces: - Core pieces:
- `packages/opencode`: OpenCode core business logic & server. - `packages/opencode`: OpenCode core business logic & server.
- `packages/tui`: The TUI code, written in Go (will be removed soon in favor of [opentui](https://github.com/sst/opentui)) - `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
- `packages/plugin`: Source for `@opencode-ai/plugin` - `packages/plugin`: Source for `@opencode-ai/plugin`
> [!NOTE] > [!NOTE]
> After touching `packages/opencode/src/server/server.ts`, the OpenCode team must regenerate the Stainless SDK before any client updates merge. > After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
## Pull Request Expectations ## Pull Request Expectations
+2
View File
@@ -126,3 +126,5 @@
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) | | 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) | | 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) | | 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
+23 -23
View File
@@ -39,7 +39,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.0.2", "version": "1.0.14",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -66,7 +66,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.0.2", "version": "1.0.14",
"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",
@@ -90,7 +90,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.0.2", "version": "1.0.14",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -111,7 +111,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.0.2", "version": "1.0.14",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -150,7 +150,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.0.2", "version": "1.0.14",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
@@ -166,7 +166,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.0.2", "version": "1.0.14",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -184,8 +184,8 @@
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opentui/core": "0.0.0-20251031-fc297165", "@opentui/core": "0.1.32",
"@opentui/solid": "0.0.0-20251031-fc297165", "@opentui/solid": "0.1.32",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:", "@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",
@@ -199,7 +199,7 @@
"fuzzysort": "3.1.0", "fuzzysort": "3.1.0",
"gray-matter": "4.0.3", "gray-matter": "4.0.3",
"hono": "catalog:", "hono": "catalog:",
"hono-openapi": "1.0.7", "hono-openapi": "1.1.1",
"ignore": "7.0.5", "ignore": "7.0.5",
"jsonc-parser": "3.3.1", "jsonc-parser": "3.3.1",
"minimatch": "10.0.3", "minimatch": "10.0.3",
@@ -243,7 +243,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.0.2", "version": "1.0.14",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"zod": "catalog:", "zod": "catalog:",
@@ -263,7 +263,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.0.2", "version": "1.0.14",
"devDependencies": { "devDependencies": {
"@hey-api/openapi-ts": "0.81.0", "@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
@@ -274,7 +274,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.0.2", "version": "1.0.14",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -287,7 +287,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.0.2", "version": "1.0.14",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -317,7 +317,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.0.2", "version": "1.0.14",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@@ -961,21 +961,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.0.0-20251031-fc297165", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251031-fc297165", "@opentui/core-darwin-x64": "0.0.0-20251031-fc297165", "@opentui/core-linux-arm64": "0.0.0-20251031-fc297165", "@opentui/core-linux-x64": "0.0.0-20251031-fc297165", "@opentui/core-win32-arm64": "0.0.0-20251031-fc297165", "@opentui/core-win32-x64": "0.0.0-20251031-fc297165", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-xtUF/uJF04d1wl4f7vsRNsDN8P9uK9Mcx1SAcm79wAN90VPNB4j2G0s7qlt8SD4zB0iWPjXICqJidjRzrQ3QVg=="], "@opentui/core": ["@opentui/core@0.1.32", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.32", "@opentui/core-darwin-x64": "0.1.32", "@opentui/core-linux-arm64": "0.1.32", "@opentui/core-linux-x64": "0.1.32", "@opentui/core-win32-arm64": "0.1.32", "@opentui/core-win32-x64": "0.1.32", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-6Ms1Gybyvl3Rt4k8OdA2c/5YlhobICcXjF5mn4k7tWujFnrBTm441G8k02pdIUffy7fD7dsouq12gfAVmSBmvA=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251031-fc297165", "", { "os": "darwin", "cpu": "arm64" }, "sha512-SD5AiofTfOT+JBx7tcBcd6BdD9sc+RPkHbhIJeqkw5V/GJ4OjyUW3m2kyR9iTs1nLMbKD5o9gyVXpLig4KmFiQ=="], "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.32", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5yvhJEsXnZGS/q2jIfz97eA4wHOJyF/zTvJL6ykvqjlXwW+bOQ8S7WcpBShR2gf+49Exak3cO+XB16yMWmCyEw=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251031-fc297165", "", { "os": "darwin", "cpu": "x64" }, "sha512-uhzxSvmfeK7vv8uNdhl8Mn2yMnjOVqdjZTOIV2aI8H9SCp8cmnzuLA8FXFO+BW6kgxsg6LbVdp4d4jDCgwtKLQ=="], "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.32", "", { "os": "darwin", "cpu": "x64" }, "sha512-DKEA3kYvFuj5C4i1N1ck+VEqYH1iCc1O958iGrc5r++jGcP0osKKEA5qSWbSlEy+iflr7Oydr350Aqyyt7J/pA=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251031-fc297165", "", { "os": "linux", "cpu": "arm64" }, "sha512-qGjjk/QTrAyqwzPC+6NhqiQZ31k3GxufbtccF8Yqan0GLuA6GrKcU72IcPwVA5t/6VIXaLkJZyFfub7CoO1D/g=="], "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.32", "", { "os": "linux", "cpu": "arm64" }, "sha512-02rgp+Rq21hg3MhQIo8guvHNGuerJgdGSWqPsUs7HwsHLL1yD8ndMxdZxOvyHNsEjzrzklQqPishHZ97QbAVYQ=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251031-fc297165", "", { "os": "linux", "cpu": "x64" }, "sha512-gre61Sxc9yX8lrqGNXz5fyE7xJHfkgDi8smGPE2OVP8HmXh0Rn1tXMzFywweEs9MELP3kdQ0VhimYJWkp8FyWg=="], "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.32", "", { "os": "linux", "cpu": "x64" }, "sha512-+ek+EYyJKC9xxVeqD14XlBYaaN7Xm45PGv7pviuqZMwJn+G6v6JxwfUNASOr/KT0N1BZdcDGp9EGToYGXwjsQg=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251031-fc297165", "", { "os": "win32", "cpu": "arm64" }, "sha512-44jsq/Ea+jIjZDXyt0w23/DkvwniQFPRB1tocGp6VrOHyHKa0IPHAQ+iuM0felbnmdMUFYyTyh1iOfAcuZyaaA=="], "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.32", "", { "os": "win32", "cpu": "arm64" }, "sha512-k9vt+jBrrWAKOWmOC02G4S8V+1iftsq6a+8+Lt9Vc6GdXNTWIKCooB5FPAoLaQeb9TXKjK1WJFgibEXo3Q9XXQ=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251031-fc297165", "", { "os": "win32", "cpu": "x64" }, "sha512-L20tCPrLFMCuX4lC2JTcixiCGFNM5RTHQwKLRcxcsSdKBr6a/7ztOG2a/2RNWkrrlbwTrUREVXH4Ivk3EOuStw=="], "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.32", "", { "os": "win32", "cpu": "x64" }, "sha512-aLUYKZVMyyyN2A0d5ETbt4ktTb6GCp4PXYHijhOXy7QPvg779H8fy7dpGUHZYEJdwAdGrIvi/y6SKn9FmuXisA=="],
"@opentui/solid": ["@opentui/solid@0.0.0-20251031-fc297165", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251031-fc297165", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-9u7ULKztDG1SvvU/wNCTFL7JYNPkG+pevcEU3JA7M2uUTIWrvKf/rD13lxtfVe7/yfxcY69SMRlaJGWpfxud5w=="], "@opentui/solid": ["@opentui/solid@0.1.32", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.32", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-JYQ7DpC1oF1jCO4I27mxYGz8M5AmBzsm7xbcG3VANA9cpCY/Tp3YXxanw2Gx1G/xSTY6QMBPhaTEuiFdIQ6FRw=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -2221,7 +2221,7 @@
"hono": ["hono@4.7.10", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="], "hono": ["hono@4.7.10", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="],
"hono-openapi": ["hono-openapi@1.0.7", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.1", "@standard-community/standard-openapi": "^0.2.4", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-rMn+nn4/HMisyi549L3zT7WCmVvmpiKsyt790GcGfqvJf9mJfhq6txw09l0IhSBxpJpA0pXVKxFijcsnGfshUA=="], "hono-openapi": ["hono-openapi@1.1.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.8", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q=="],
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
+45 -15
View File
@@ -168,7 +168,9 @@ try {
const summary = await summarize(response) const summary = await summarize(response)
await pushToLocalBranch(summary) await pushToLocalBranch(summary)
} }
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${useShareUrl()}/s/${shareId}`)) const hasShared = prData.comments.nodes.some((c) =>
c.body.includes(`${useShareUrl()}/s/${shareId}`),
)
await updateComment(`${response}${footer({ image: !hasShared })}`) await updateComment(`${response}${footer({ image: !hasShared })}`)
} }
// Fork PR // Fork PR
@@ -180,7 +182,9 @@ try {
const summary = await summarize(response) const summary = await summarize(response)
await pushToForkBranch(summary, prData) await pushToForkBranch(summary, prData)
} }
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${useShareUrl()}/s/${shareId}`)) const hasShared = prData.comments.nodes.some((c) =>
c.body.includes(`${useShareUrl()}/s/${shareId}`),
)
await updateComment(`${response}${footer({ image: !hasShared })}`) await updateComment(`${response}${footer({ image: !hasShared })}`)
} }
} }
@@ -361,7 +365,9 @@ async function getAccessToken() {
if (!response.ok) { if (!response.ok) {
const responseJson = (await response.json()) as { error?: string } const responseJson = (await response.json()) as { error?: string }
throw new Error(`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`) throw new Error(
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`,
)
} }
const responseJson = (await response.json()) as { token: string } const responseJson = (await response.json()) as { token: string }
@@ -402,8 +408,12 @@ async function getUserPrompt() {
// ie. <img alt="Image" src="https://github.com/user-attachments/assets/xxxx" /> // ie. <img alt="Image" src="https://github.com/user-attachments/assets/xxxx" />
// ie. [api.json](https://github.com/user-attachments/files/21433810/api.json) // ie. [api.json](https://github.com/user-attachments/files/21433810/api.json)
// ie. ![Image](https://github.com/user-attachments/assets/xxxx) // ie. ![Image](https://github.com/user-attachments/assets/xxxx)
const mdMatches = prompt.matchAll(/!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi) const mdMatches = prompt.matchAll(
const tagMatches = prompt.matchAll(/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi) /!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi,
)
const tagMatches = prompt.matchAll(
/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi,
)
const matches = [...mdMatches, ...tagMatches].sort((a, b) => a.index - b.index) const matches = [...mdMatches, ...tagMatches].sort((a, b) => a.index - b.index)
console.log("Images", JSON.stringify(matches, null, 2)) console.log("Images", JSON.stringify(matches, null, 2))
@@ -430,7 +440,8 @@ async function getUserPrompt() {
// Replace img tag with file path, ie. @image.png // Replace img tag with file path, ie. @image.png
const replacement = `@${filename}` const replacement = `@${filename}`
prompt = prompt.slice(0, start + offset) + replacement + prompt.slice(start + offset + tag.length) prompt =
prompt.slice(0, start + offset) + replacement + prompt.slice(start + offset + tag.length)
offset += replacement.length - tag.length offset += replacement.length - tag.length
const contentType = res.headers.get("content-type") const contentType = res.headers.get("content-type")
@@ -498,7 +509,12 @@ async function subscribeSessionEvents() {
? JSON.stringify(part.state.input) ? JSON.stringify(part.state.input)
: "Unknown" : "Unknown"
console.log() console.log()
console.log(color + `|`, "\x1b[0m\x1b[2m" + ` ${tool.padEnd(7, " ")}`, "", "\x1b[0m" + title) console.log(
color + `|`,
"\x1b[0m\x1b[2m" + ` ${tool.padEnd(7, " ")}`,
"",
"\x1b[0m" + title,
)
} }
if (part.type === "text") { if (part.type === "text") {
@@ -710,7 +726,8 @@ async function assertPermissions() {
throw new Error(`Failed to check permissions for user ${actor}: ${error}`) throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
} }
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`) if (!["admin", "write"].includes(permission))
throw new Error(`User ${actor} does not have write permissions`)
} }
async function updateComment(body: string) { async function updateComment(body: string) {
@@ -730,12 +747,13 @@ async function updateComment(body: string) {
async function createPR(base: string, branch: string, title: string, body: string) { async function createPR(base: string, branch: string, title: string, body: string) {
console.log("Creating pull request...") console.log("Creating pull request...")
const { repo } = useContext() const { repo } = useContext()
const truncatedTitle = title.length > 256 ? title.slice(0, 253) + "..." : title
const pr = await octoRest.rest.pulls.create({ const pr = await octoRest.rest.pulls.create({
owner: repo.owner, owner: repo.owner,
repo: repo.repo, repo: repo.repo,
head: branch, head: branch,
base, base,
title, title: truncatedTitle,
body, body,
}) })
return pr.data.number return pr.data.number
@@ -753,7 +771,9 @@ function footer(opts?: { image?: boolean }) {
return `<a href="${useShareUrl()}/s/${shareId}"><img width="200" alt="${titleAlt}" src="https://social-cards.sst.dev/opencode-share/${title64}.png?model=${providerID}/${modelID}&version=${session.version}&id=${shareId}" /></a>\n` return `<a href="${useShareUrl()}/s/${shareId}"><img width="200" alt="${titleAlt}" src="https://social-cards.sst.dev/opencode-share/${title64}.png?model=${providerID}/${modelID}&version=${session.version}&id=${shareId}" /></a>\n`
})() })()
const shareUrl = shareId ? `[opencode session](${useShareUrl()}/s/${shareId})&nbsp;&nbsp;|&nbsp;&nbsp;` : "" const shareUrl = shareId
? `[opencode session](${useShareUrl()}/s/${shareId})&nbsp;&nbsp;|&nbsp;&nbsp;`
: ""
return `\n\n${image}${shareUrl}[github run](${useEnvRunUrl()})` return `\n\n${image}${shareUrl}[github run](${useEnvRunUrl()})`
} }
@@ -936,9 +956,13 @@ function buildPromptDataForPR(pr: GitHubPullRequest) {
}) })
.map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`) .map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`)
const files = (pr.files.nodes || []).map((f) => `- ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`) const files = (pr.files.nodes || []).map(
(f) => `- ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`,
)
const reviewData = (pr.reviews.nodes || []).map((r) => { const reviewData = (pr.reviews.nodes || []).map((r) => {
const comments = (r.comments.nodes || []).map((c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`) const comments = (r.comments.nodes || []).map(
(c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`,
)
return [ return [
`- ${r.author.login} at ${r.submittedAt}:`, `- ${r.author.login} at ${r.submittedAt}:`,
` - Review body: ${r.body}`, ` - Review body: ${r.body}`,
@@ -960,9 +984,15 @@ function buildPromptDataForPR(pr: GitHubPullRequest) {
`Deletions: ${pr.deletions}`, `Deletions: ${pr.deletions}`,
`Total Commits: ${pr.commits.totalCount}`, `Total Commits: ${pr.commits.totalCount}`,
`Changed Files: ${pr.files.nodes.length} files`, `Changed Files: ${pr.files.nodes.length} files`,
...(comments.length > 0 ? ["<pull_request_comments>", ...comments, "</pull_request_comments>"] : []), ...(comments.length > 0
...(files.length > 0 ? ["<pull_request_changed_files>", ...files, "</pull_request_changed_files>"] : []), ? ["<pull_request_comments>", ...comments, "</pull_request_comments>"]
...(reviewData.length > 0 ? ["<pull_request_reviews>", ...reviewData, "</pull_request_reviews>"] : []), : []),
...(files.length > 0
? ["<pull_request_changed_files>", ...files, "</pull_request_changed_files>"]
: []),
...(reviewData.length > 0
? ["<pull_request_reviews>", ...reviewData, "</pull_request_reviews>"]
: []),
"</pull_request>", "</pull_request>",
].join("\n") ].join("\n")
} }
+1 -1
View File
@@ -4,7 +4,7 @@
"description": "AI-powered development tool", "description": "AI-powered development tool",
"private": true, "private": true,
"type": "module", "type": "module",
"packageManager": "bun@1.3.0", "packageManager": "bun@1.3.1",
"scripts": { "scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"typecheck": "bun turbo typecheck", "typecheck": "bun turbo typecheck",
+1 -1
View File
@@ -7,7 +7,7 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json", "build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start", "start": "vinxi start",
"version": "1.0.2" "version": "1.0.14"
}, },
"dependencies": { "dependencies": {
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
+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/console-core", "name": "@opencode-ai/console-core",
"version": "1.0.2", "version": "1.0.14",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.0.2", "version": "1.0.14",
"$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-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.0.2", "version": "1.0.14",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.0.2", "version": "1.0.14",
"description": "", "description": "",
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -1,34 +1,64 @@
import { For, Match, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" import { For, JSXElement, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
import { Part } from "@opencode-ai/ui" import { Markdown, Part } from "@opencode-ai/ui"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import type { AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk" import type { AssistantMessage as AssistantMessageType, Part as PartType, ToolPart } from "@opencode-ai/sdk"
import { Spinner } from "./spinner"
export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[] }) { export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) {
const sync = useSync() const sync = useSync()
const items = createMemo(() => props.assistantMessages().flatMap((m) => sync.data.part[m.id])) const parts = createMemo(() => props.assistantMessages().flatMap((m) => sync.data.part[m.id]))
const done = createMemo(() => props.done ?? false)
const currentTask = createMemo(
() =>
parts().findLast(
(p) =>
p &&
p.type === "tool" &&
p.tool === "task" &&
p.state &&
"metadata" in p.state &&
p.state.metadata &&
p.state.metadata.sessionId &&
p.state.status === "running",
) as ToolPart,
)
const finishedItems = createMemo(() => [ const resolvedParts = createMemo(() => {
"", let resolved = parts()
"", const task = currentTask()
"Loading...", if (task && task.state && "metadata" in task.state && task.state.metadata?.sessionId) {
...items().filter( const messages = sync.data.message[task.state.metadata.sessionId as string]?.filter((m) => m.role === "assistant")
(p) => resolved = messages?.flatMap((m) => sync.data.part[m.id]) ?? parts()
p?.type === "text" || }
(p?.type === "reasoning" && p.time?.end) || return resolved
(p?.type === "tool" && p.state.status === "completed"), })
), const currentText = createMemo(
"", () =>
resolvedParts().findLast((p) => p?.type === "text")?.text ||
resolvedParts().findLast((p) => p?.type === "reasoning")?.text,
)
const eligibleItems = createMemo(() => {
return resolvedParts().filter((p) => p?.type === "tool" && p.state.status === "completed")
})
const finishedItems = createMemo<(JSXElement | PartType)[]>(() => [
<div class="h-8 w-full" />,
<div class="h-8 w-full" />,
<div class="flex items-center gap-x-5 pl-3 text-text-base">
<Spinner /> <span class="text-12-medium">Thinking...</span>
</div>,
...eligibleItems(),
...(done() ? [<div class="h-8 w-full" />, <div class="h-8 w-full" />, <div class="h-8 w-full" />] : []),
]) ])
const MINIMUM_DELAY = 400 const delay = createMemo(() => (done() ? 220 : 400))
const [visibleCount, setVisibleCount] = createSignal(1) const [visibleCount, setVisibleCount] = createSignal(eligibleItems().length)
createEffect(() => { createEffect(() => {
const total = finishedItems().length const total = finishedItems().length
if (total > visibleCount()) { if (total > visibleCount()) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
setVisibleCount((prev) => prev + 1) setVisibleCount((prev) => prev + 1)
}, MINIMUM_DELAY) }, delay())
onCleanup(() => clearTimeout(timer)) onCleanup(() => clearTimeout(timer))
} else if (total < visibleCount()) { } else if (total < visibleCount()) {
setVisibleCount(total) setVisibleCount(total)
@@ -42,41 +72,57 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa
}) })
return ( return (
<div <div class="flex flex-col gap-3">
class="h-30 overflow-hidden pointer-events-none
mask-alpha mask-t-from-33% mask-t-from-background-base mask-t-to-transparent
mask-b-from-90% mask-b-from-background-base mask-b-to-transparent"
>
<div <div
class="w-full flex flex-col items-start self-stretch gap-2 py-8 class="h-30 overflow-hidden pointer-events-none pb-1
transform transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]" mask-alpha mask-t-from-33% mask-t-from-background-base mask-t-to-transparent
style={{ transform: `translateY(${translateY()})` }} mask-b-from-95% mask-b-from-background-base mask-b-to-transparent"
> >
<For each={finishedItems()}> <div
{(part) => { class="w-full flex flex-col items-start self-stretch gap-2 py-8
if (typeof part === "string") return <div class="h-8 flex items-center w-full">{part}</div> transform transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
const message = createMemo(() => sync.data.message[part.sessionID].find((m) => m.id === part.messageID)) style={{ transform: `translateY(${translateY()})` }}
return ( >
<div class="h-8 flex items-center w-full"> <For each={finishedItems()}>
<Switch> {(part) => {
<Match when={part.type === "text" && part}> if (part && typeof part === "object" && "type" in part) {
{(p) => ( const message = createMemo(() => sync.data.message[part.sessionID].find((m) => m.id === part.messageID))
<div return (
textContent={p().text} <div class="h-8 flex items-center w-full">
class="text-12-regular text-text-base whitespace-nowrap truncate w-full" <Switch>
/> <Match when={part.type === "text" && part}>
)} {(p) => (
</Match> <div
<Match when={part.type === "reasoning" && part}> textContent={p().text}
{(p) => <Part message={message()!} part={p()} />} class="text-12-regular text-text-base whitespace-nowrap truncate w-full"
</Match> />
<Match when={part.type === "tool" && part}>{(p) => <Part message={message()!} part={p()} />}</Match> )}
</Switch> </Match>
</div> <Match when={part.type === "reasoning" && part}>
) {(p) => <Part message={message()!} part={p()} />}
}} </Match>
</For> <Match when={part.type === "tool" && part}>
{(p) => <Part message={message()!} part={p()} />}
</Match>
</Switch>
</div>
)
}
return <div class="h-8 flex items-center w-full">{part}</div>
}}
</For>
</div>
</div> </div>
<Show when={currentText()}>
{(text) => (
<div
class="max-h-36 flex flex-col justify-end overflow-hidden py-3
mask-alpha mask-t-from-80% mask-t-from-background-base mask-t-to-transparent"
>
<Markdown text={text()} class="w-full shrink-0 overflow-visible" />
</div>
)}
</Show>
</div> </div>
) )
} }
@@ -334,7 +334,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onSubmit={handleSubmit} onSubmit={handleSubmit}
classList={{ classList={{
"bg-surface-raised-stronger-non-alpha border border-border-strong-base": true, "bg-surface-raised-stronger-non-alpha border border-border-strong-base": true,
"rounded-2xl overflow-clip focus-within:shadow-xs-border-selected": true, "rounded-2xl overflow-clip focus-within:border-transparent focus-within:shadow-xs-border-select": true,
[props.class ?? ""]: !!props.class, [props.class ?? ""]: !!props.class,
}} }}
> >
+22
View File
@@ -162,10 +162,32 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const recent = createMemo(() => store.recent.map(find).filter(Boolean)) const recent = createMemo(() => store.recent.map(find).filter(Boolean))
const cycle = (direction: 1 | -1) => {
const recentList = recent()
const current = currentModel()
if (!current) return
const index = recentList.findIndex((x) => x?.provider.id === current.provider.id && x?.id === current.id)
if (index === -1) return
let next = index + direction
if (next < 0) next = recentList.length - 1
if (next >= recentList.length) next = 0
const val = recentList[next]
if (!val) return
model.set({
providerID: val.provider.id,
modelID: val.id,
})
}
return { return {
current: currentModel, current: currentModel,
recent, recent,
list, list,
cycle,
set(model: ModelKey | undefined, options?: { recent?: boolean }) { set(model: ModelKey | undefined, options?: { recent?: boolean }) {
batch(() => { batch(() => {
setStore("model", agent.current().name, model ?? fallbackModel()) setStore("model", agent.current().name, model ?? fallbackModel())
+114 -16
View File
@@ -9,7 +9,6 @@ import {
Accordion, Accordion,
Diff, Diff,
Collapsible, Collapsible,
Part,
DiffChanges, DiffChanges,
ProgressCircle, ProgressCircle,
Message, Message,
@@ -548,7 +547,79 @@ export default function Page() {
<For each={local.session.userMessages()}> <For each={local.session.userMessages()}>
{(message) => { {(message) => {
const diffs = createMemo(() => message.summary?.diffs ?? []) const diffs = createMemo(() => message.summary?.diffs ?? [])
const working = createMemo(() => !message.summary?.title) const working = createMemo(() => !message.summary?.body)
const assistantMessages = createMemo(() => {
return sync.data.message[activeSession().id]?.filter(
(m) => m.role === "assistant" && m.parentID == message.id,
) as AssistantMessageType[]
})
const parts = createMemo(() =>
assistantMessages().flatMap((m) => sync.data.part[m.id]),
)
const lastPart = createMemo(() => parts().slice(-1)?.at(0))
const rawStatus = createMemo(() => {
const defaultStatus = "Working..."
const last = lastPart()
if (!last) return defaultStatus
if (last.type === "tool") {
switch (last.tool) {
case "task":
return "Delegating work..."
case "todowrite":
case "todoread":
return "Planning next steps..."
case "read":
return "Gathering context..."
case "list":
case "grep":
case "glob":
return "Searching the codebase..."
case "webfetch":
return "Searching the web..."
case "edit":
case "write":
return "Making edits..."
case "bash":
return "Running commands..."
default:
break
}
} else if (last.type === "reasoning") {
return "Thinking..."
} else if (last.type === "text") {
return "Gathering thoughts..."
}
return defaultStatus
})
const [status, setStatus] = createSignal(rawStatus())
let lastStatusChange = Date.now()
let statusTimeout: number | undefined
createEffect(() => {
const newStatus = rawStatus()
if (newStatus === status()) return
const timeSinceLastChange = Date.now() - lastStatusChange
if (timeSinceLastChange >= 1000) {
setStatus(newStatus)
lastStatusChange = Date.now()
if (statusTimeout) {
clearTimeout(statusTimeout)
statusTimeout = undefined
}
} else {
if (statusTimeout) clearTimeout(statusTimeout)
statusTimeout = setTimeout(() => {
setStatus(rawStatus())
lastStatusChange = Date.now()
statusTimeout = undefined
}, 1000 - timeSinceLastChange) as unknown as number
}
})
return ( return (
<li class="group/li flex items-center self-stretch"> <li class="group/li flex items-center self-stretch">
<button <button
@@ -570,7 +641,10 @@ export default function Page() {
"text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true, "text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
}} }}
> >
{message.summary?.title ?? local.session.getMessageText(message)} <Switch>
<Match when={working()}>{status()}</Match>
<Match when={true}>{message.summary?.title}</Match>
</Switch>
</div> </div>
</button> </button>
</li> </li>
@@ -583,7 +657,8 @@ export default function Page() {
<For each={local.session.userMessages()}> <For each={local.session.userMessages()}>
{(message) => { {(message) => {
const isActive = createMemo(() => local.session.activeMessage()?.id === message.id) const isActive = createMemo(() => local.session.activeMessage()?.id === message.id)
const [initialized, setInitialized] = createSignal(!!message.summary?.title) const [titled, setTitled] = createSignal(!!message.summary?.title)
const [completed, setCompleted] = createSignal(!!message.summary?.body)
const [expanded, setExpanded] = createSignal(false) const [expanded, setExpanded] = createSignal(false)
const parts = createMemo(() => sync.data.part[message.id]) const parts = createMemo(() => sync.data.part[message.id])
const title = createMemo(() => message.summary?.title) const title = createMemo(() => message.summary?.title)
@@ -597,11 +672,18 @@ export default function Page() {
const hasToolPart = createMemo(() => const hasToolPart = createMemo(() =>
assistantMessages() assistantMessages()
?.flatMap((m) => sync.data.part[m.id]) ?.flatMap((m) => sync.data.part[m.id])
.some((p) => p.type === "tool"), .some((p) => p?.type === "tool"),
) )
const working = createMemo(() => !summary()) const working = createMemo(() => !summary())
// allowing time for the animations to finish
createEffect(() => { createEffect(() => {
setTimeout(() => setInitialized(!!title()), 10_000) title()
setTimeout(() => setTitled(!!title()), 10_000)
})
createEffect(() => {
summary()
setTimeout(() => setCompleted(!!summary()), 1200)
}) })
return ( return (
@@ -612,9 +694,18 @@ export default function Page() {
> >
{/* Title */} {/* Title */}
<div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10"> <div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10">
<div class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0"> <div class="w-full text-14-medium text-text-strong">
<Show when={initialized()} fallback={<Typewriter as="h1" text={title()} />}> <Show
<h1>{title()}</h1> when={titled()}
fallback={
<Typewriter
as="h1"
text={title()}
class="overflow-hidden text-ellipsis min-w-0 text-nowrap"
/>
}
>
<h1 class="overflow-hidden text-ellipsis min-w-0 text-nowrap">{title()}</h1>
</Show> </Show>
</div> </div>
</div> </div>
@@ -622,7 +713,7 @@ export default function Page() {
<Message message={message} parts={parts()} /> <Message message={message} parts={parts()} />
</div> </div>
{/* Summary */} {/* Summary */}
<Show when={!working()}> <Show when={completed()}>
<div class="w-full flex flex-col gap-6 items-start self-stretch"> <div class="w-full flex flex-col gap-6 items-start self-stretch">
<div class="flex flex-col items-start gap-1 self-stretch"> <div class="flex flex-col items-start gap-1 self-stretch">
<h2 class="text-12-medium text-text-weak"> <h2 class="text-12-medium text-text-weak">
@@ -631,7 +722,14 @@ export default function Page() {
<Match when={true}>Response</Match> <Match when={true}>Response</Match>
</Switch> </Switch>
</h2> </h2>
<Show when={summary()}>{(summary) => <Markdown text={summary()} />}</Show> <Show when={summary()}>
{(summary) => (
<Markdown
classList={{ "[&>*]:fade-up-text": !diffs().length }}
text={summary()}
/>
)}
</Show>
</div> </div>
<Accordion class="w-full" multiple> <Accordion class="w-full" multiple>
<For each={diffs()}> <For each={diffs()}>
@@ -684,17 +782,17 @@ export default function Page() {
{/* Response */} {/* Response */}
<div class="w-full"> <div class="w-full">
<Switch> <Switch>
<Match when={working()}> <Match when={!completed()}>
<MessageProgress assistantMessages={assistantMessages} /> <MessageProgress assistantMessages={assistantMessages} done={!working()} />
</Match> </Match>
<Match when={!working() && hasToolPart()}> <Match when={completed() && hasToolPart()}>
<Collapsible variant="ghost" open={expanded()} onOpenChange={setExpanded}> <Collapsible variant="ghost" open={expanded()} onOpenChange={setExpanded}>
<Collapsible.Trigger class="text-text-weak hover:text-text-strong"> <Collapsible.Trigger class="text-text-weak hover:text-text-strong">
<div class="flex items-center gap-1 self-stretch"> <div class="flex items-center gap-1 self-stretch">
<div class="text-12-medium"> <div class="text-12-medium">
<Switch> <Switch>
<Match when={expanded()}>Hide steps</Match> <Match when={expanded()}>Hide details</Match>
<Match when={!expanded()}>Show steps</Match> <Match when={!expanded()}>Show details</Match>
</Switch> </Switch>
</div> </div>
<Collapsible.Arrow /> <Collapsible.Arrow />
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.0.2", "version": "1.0.14",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.0.2", "version": "1.0.14",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"private": true, "private": true,
@@ -54,8 +54,8 @@
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opentui/core": "0.0.0-20251031-fc297165", "@opentui/core": "0.1.32",
"@opentui/solid": "0.0.0-20251031-fc297165", "@opentui/solid": "0.1.32",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",
"@pierre/precision-diffs": "catalog:", "@pierre/precision-diffs": "catalog:",
@@ -69,7 +69,7 @@
"fuzzysort": "3.1.0", "fuzzysort": "3.1.0",
"gray-matter": "4.0.3", "gray-matter": "4.0.3",
"hono": "catalog:", "hono": "catalog:",
"hono-openapi": "1.0.7", "hono-openapi": "1.1.1",
"ignore": "7.0.5", "ignore": "7.0.5",
"jsonc-parser": "3.3.1", "jsonc-parser": "3.3.1",
"minimatch": "10.0.3", "minimatch": "10.0.3",
+2 -4
View File
@@ -125,10 +125,8 @@ if (!Script.preview) {
"build() {", "build() {",
` cd "opencode-\${pkgver}"`, ` cd "opencode-\${pkgver}"`,
` bun install`, ` bun install`,
" cd packages/tui", " cd ./packages/opencode",
` CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=\${pkgver}" -o tui cmd/opencode/main.go`, ` OPENCODE_CHANNEL=latest OPENCODE_VERSION=${pkgver} bun run ./script/build.ts --single`,
" cd ../opencode",
` bun build --define OPENCODE_TUI_PATH="'$(realpath ../tui/tui)'" --define OPENCODE_VERSION="'\${pkgver}'" --compile --target=bun-linux-x64 --outfile=opencode ./src/index.ts`,
"}", "}",
"", "",
"package() {", "package() {",
+18 -22
View File
@@ -28,8 +28,6 @@ import { Storage } from "@/storage/storage"
import { Command } from "@/command" import { Command } from "@/command"
import { Agent as Agents } from "@/agent/agent" import { Agent as Agents } from "@/agent/agent"
import { Permission } from "@/permission" import { Permission } from "@/permission"
import { Session } from "@/session"
import { Identifier } from "@/id/id"
import { SessionCompaction } from "@/session/compaction" import { SessionCompaction } from "@/session/compaction"
import type { Config } from "@/config/config" import type { Config } from "@/config/config"
import { MCP } from "@/mcp" import { MCP } from "@/mcp"
@@ -89,7 +87,11 @@ export namespace ACP {
}) })
if (!res) return if (!res) return
if (res.outcome.outcome !== "selected") { if (res.outcome.outcome !== "selected") {
Permission.respond({ sessionID: permission.sessionID, permissionID: permission.id, response: "reject" }) Permission.respond({
sessionID: permission.sessionID,
permissionID: permission.id,
response: "reject",
})
return return
} }
Permission.respond({ Permission.respond({
@@ -111,9 +113,11 @@ export namespace ACP {
const acpSession = this.sessionManager.get(part.sessionID) const acpSession = this.sessionManager.get(part.sessionID)
if (!acpSession) return if (!acpSession) return
const message = await Storage.read<MessageV2.Info>(["message", part.sessionID, part.messageID]).catch( const message = await Storage.read<MessageV2.Info>([
() => undefined, "message",
) part.sessionID,
part.messageID,
]).catch(() => undefined)
if (!message || message.role !== "assistant") return if (!message || message.role !== "assistant") return
if (part.type === "tool") { if (part.type === "tool") {
@@ -192,7 +196,9 @@ export namespace ACP {
sessionUpdate: "plan", sessionUpdate: "plan",
entries: parsedTodos.data.map((todo) => { entries: parsedTodos.data.map((todo) => {
const status: PlanEntry["status"] = const status: PlanEntry["status"] =
todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"]) todo.status === "cancelled"
? "completed"
: (todo.status as PlanEntry["status"])
return { return {
priority: "medium", priority: "medium",
status, status,
@@ -375,11 +381,6 @@ export namespace ACP {
description: command.description ?? "", description: command.description ?? "",
})) }))
const names = new Set(availableCommands.map((c) => c.name)) const names = new Set(availableCommands.map((c) => c.name))
if (!names.has("init"))
availableCommands.push({
name: "init",
description: "create/update a AGENTS.md",
})
if (!names.has("compact")) if (!names.has("compact"))
availableCommands.push({ availableCommands.push({
name: "compact", name: "compact",
@@ -404,7 +405,8 @@ export namespace ACP {
description: agent.description, description: agent.description,
})) }))
const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id const currentModeId =
availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
const mcpServers: Record<string, Config.Mcp> = {} const mcpServers: Record<string, Config.Mcp> = {}
for (const server of params.mcpServers) { for (const server of params.mcpServers) {
@@ -585,14 +587,6 @@ export namespace ACP {
} }
switch (cmd.name) { switch (cmd.name) {
case "init":
await Session.initialize({
sessionID,
messageID: Identifier.ascending("message"),
providerID: model.providerID,
modelID: model.modelID,
})
break
case "compact": case "compact":
await SessionCompaction.run({ await SessionCompaction.run({
sessionID, sessionID,
@@ -665,7 +659,9 @@ export namespace ACP {
function parseUri( function parseUri(
uri: string, uri: string,
): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } { ):
| { type: "file"; url: string; filename: string; mime: string }
| { type: "text"; text: string } {
try { try {
if (uri.startsWith("file://")) { if (uri.startsWith("file://")) {
const path = uri.slice(7) const path = uri.slice(7)
-1
View File
@@ -12,7 +12,6 @@ export const ServeCommand = cmd({
default: 0, default: 0,
}) })
.option("hostname", { .option("hostname", {
alias: ["h"],
type: "string", type: "string",
describe: "hostname to listen on", describe: "hostname to listen on",
default: "127.0.0.1", default: "127.0.0.1",
-1
View File
@@ -65,7 +65,6 @@ export const TuiCommand = cmd({
default: 0, default: 0,
}) })
.option("hostname", { .option("hostname", {
alias: ["h"],
type: "string", type: "string",
describe: "hostname to listen on", describe: "hostname to listen on",
default: "127.0.0.1", default: "127.0.0.1",
+169 -10
View File
@@ -23,21 +23,81 @@ import { Session } from "@tui/routes/session"
import { PromptHistoryProvider } from "./component/prompt/history" import { PromptHistoryProvider } from "./component/prompt/history"
import { DialogAlert } from "./ui/dialog-alert" import { DialogAlert } from "./ui/dialog-alert"
import { ToastProvider, useToast } from "./ui/toast" import { ToastProvider, useToast } from "./ui/toast"
import { ExitProvider } from "./context/exit" import { ExitProvider, useExit } from "./context/exit"
import type { SessionRoute } from "./context/route" import type { SessionRoute } from "./context/route"
import { Session as SessionApi } from "@/session" import { Session as SessionApi } from "@/session"
import { TuiEvent } from "./event" import { TuiEvent } from "./event"
import { KVProvider, useKV } from "./context/kv" import { KVProvider, useKV } from "./context/kv"
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
return new Promise((resolve) => {
let timeout: NodeJS.Timeout
const cleanup = () => {
process.stdin.setRawMode(false)
process.stdin.removeListener("data", handler)
clearTimeout(timeout)
}
const handler = (data: Buffer) => {
const str = data.toString()
const match = str.match(/\x1b]11;([^\x07\x1b]+)/)
if (match) {
cleanup()
const color = match[1]
// Parse RGB values from color string
// Formats: rgb:RR/GG/BB or #RRGGBB or rgb(R,G,B)
let r = 0,
g = 0,
b = 0
if (color.startsWith("rgb:")) {
const parts = color.substring(4).split("/")
r = parseInt(parts[0], 16) >> 8 // Convert 16-bit to 8-bit
g = parseInt(parts[1], 16) >> 8 // Convert 16-bit to 8-bit
b = parseInt(parts[2], 16) >> 8 // Convert 16-bit to 8-bit
} else if (color.startsWith("#")) {
r = parseInt(color.substring(1, 3), 16)
g = parseInt(color.substring(3, 5), 16)
b = parseInt(color.substring(5, 7), 16)
} else if (color.startsWith("rgb(")) {
const parts = color.substring(4, color.length - 1).split(",")
r = parseInt(parts[0])
g = parseInt(parts[1])
b = parseInt(parts[2])
}
// Calculate luminance using relative luminance formula
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
// Determine if dark or light based on luminance threshold
resolve(luminance > 0.5 ? "light" : "dark")
}
}
process.stdin.setRawMode(true)
process.stdin.on("data", handler)
process.stdout.write("\x1b]11;?\x07")
timeout = setTimeout(() => {
cleanup()
resolve("dark")
}, 1000)
})
}
export function tui(input: { export function tui(input: {
url: string url: string
sessionID?: string sessionID?: string
model?: string model?: string
agent?: string agent?: string
prompt?: string
onExit?: () => Promise<void> onExit?: () => Promise<void>
}) { }) {
// promise to prevent immediate exit // promise to prevent immediate exit
return new Promise<void>((resolve) => { return new Promise<void>(async (resolve) => {
const mode = await getTerminalBackgroundColor()
const routeData: Route | undefined = input.sessionID const routeData: Route | undefined = input.sessionID
? { ? {
type: "session", type: "session",
@@ -53,15 +113,23 @@ export function tui(input: {
render( render(
() => { () => {
return ( return (
<ErrorBoundary fallback={<text>Something went wrong</text>}> <ErrorBoundary
fallback={(error, reset) => (
<ErrorComponent error={error} reset={reset} onExit={onExit} />
)}
>
<ExitProvider onExit={onExit}> <ExitProvider onExit={onExit}>
<KVProvider> <KVProvider>
<ToastProvider> <ToastProvider>
<RouteProvider data={routeData}> <RouteProvider data={routeData}>
<SDKProvider url={input.url}> <SDKProvider url={input.url}>
<SyncProvider> <SyncProvider>
<ThemeProvider> <ThemeProvider mode={mode}>
<LocalProvider initialModel={input.model} initialAgent={input.agent}> <LocalProvider
initialModel={input.model}
initialAgent={input.agent}
initialPrompt={input.prompt}
>
<KeybindProvider> <KeybindProvider>
<DialogProvider> <DialogProvider>
<CommandProvider> <CommandProvider>
@@ -86,6 +154,7 @@ export function tui(input: {
targetFps: 60, targetFps: 60,
gatherStats: false, gatherStats: false,
exitOnCtrlC: false, exitOnCtrlC: false,
useKittyKeyboard: true,
}, },
) )
}) })
@@ -104,7 +173,8 @@ function App() {
const sync = useSync() const sync = useSync()
const toast = useToast() const toast = useToast()
const [sessionExists, setSessionExists] = createSignal(false) const [sessionExists, setSessionExists] = createSignal(false)
const { theme } = useTheme() const { theme, mode, setMode } = useTheme()
const exit = useExit()
useKeyboard(async (evt) => { useKeyboard(async (evt) => {
if (evt.meta && evt.name === "t") { if (evt.meta && evt.name === "t") {
@@ -168,6 +238,24 @@ function App() {
dialog.replace(() => <DialogModel />) dialog.replace(() => <DialogModel />)
}, },
}, },
{
title: "Model cycle",
value: "model.cycle_recent",
keybind: "model_cycle_recent",
category: "Agent",
onSelect: () => {
local.model.cycle(1)
},
},
{
title: "Model cycle reverse",
value: "model.cycle_recent_reverse",
keybind: "model_cycle_recent_reverse",
category: "Agent",
onSelect: () => {
local.model.cycle(-1)
},
},
{ {
title: "Switch agent", title: "Switch agent",
value: "agent.list", value: "agent.list",
@@ -214,6 +302,14 @@ function App() {
}, },
category: "System", category: "System",
}, },
{
title: `Switch to ${mode() === "dark" ? "light" : "dark"} mode`,
value: "theme.switch_mode",
onSelect: () => {
setMode(mode() === "dark" ? "light" : "dark")
},
category: "System",
},
{ {
title: "Help", title: "Help",
value: "help.show", value: "help.show",
@@ -222,11 +318,17 @@ function App() {
}, },
category: "System", category: "System",
}, },
{
title: "Exit the app",
value: "app.exit",
onSelect: exit,
category: "System",
},
]) ])
createEffect(() => { createEffect(() => {
const providerID = local.model.current().providerID const providerID = local.model.current().providerID
if (providerID === "openrouter" && !kv.data.openrouter_warning) { if (providerID === "openrouter" && !kv.get("openrouter_warning", false)) {
untrack(() => { untrack(() => {
DialogAlert.show( DialogAlert.show(
dialog, dialog,
@@ -274,8 +376,9 @@ function App() {
/* @ts-expect-error */ /* @ts-expect-error */
renderer.writeOut(finalOsc52) renderer.writeOut(finalOsc52)
await Clipboard.copy(text) await Clipboard.copy(text)
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
.catch(toast.error)
renderer.clearSelection() renderer.clearSelection()
toast.show({ message: "Copied to clipboard", variant: "info" })
} }
}} }}
> >
@@ -304,7 +407,9 @@ function App() {
paddingRight={1} paddingRight={1}
> >
<text fg={theme.textMuted}>open</text> <text fg={theme.textMuted}>open</text>
<text attributes={TextAttributes.BOLD}>code </text> <text fg={theme.text} attributes={TextAttributes.BOLD}>
code{" "}
</text>
<text fg={theme.textMuted}>v{Installation.VERSION}</text> <text fg={theme.textMuted}>v{Installation.VERSION}</text>
</box> </box>
<box paddingLeft={1} paddingRight={1}> <box paddingLeft={1} paddingRight={1}>
@@ -319,7 +424,7 @@ function App() {
<text <text
bg={local.agent.color(local.agent.current().name)} bg={local.agent.color(local.agent.current().name)}
fg={theme.background} fg={theme.background}
wrapMode="none" wrapMode={undefined}
> >
<span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span> <span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span>
<span> AGENT </span> <span> AGENT </span>
@@ -329,3 +434,57 @@ function App() {
</box> </box>
) )
} }
function ErrorComponent(props: { error: Error; reset: () => void; onExit: () => Promise<void> }) {
const term = useTerminalDimensions()
useKeyboard((evt) => {
if (evt.ctrl && evt.name === "c") {
props.onExit()
}
})
const [copied, setCopied] = createSignal(false)
const issueURL = new URL("https://github.com/sst/opencode/issues/new?template=bug-report.yml")
if (props.error.message) {
issueURL.searchParams.set("title", `opentui: fatal: ${props.error.message}`)
}
if (props.error.stack) {
issueURL.searchParams.set(
"description",
"```\n" + props.error.stack.substring(0, 6000 - issueURL.toString().length) + "...\n```",
)
}
const copyIssueURL = () => {
Clipboard.copy(issueURL.toString()).then(() => {
setCopied(true)
})
}
return (
<box flexDirection="column" gap={1}>
<box flexDirection="row" gap={1} alignItems="center">
<text attributes={TextAttributes.BOLD}>Please report an issue.</text>
<box onMouseUp={copyIssueURL} backgroundColor="#565f89" padding={1}>
<text attributes={TextAttributes.BOLD}>Copy issue URL (exception info pre-filled)</text>
</box>
{copied() && <text>Successfully copied</text>}
</box>
<box flexDirection="row" gap={2} alignItems="center">
<text>A fatal error occurred!</text>
<box onMouseUp={props.reset} backgroundColor="#565f89" padding={1}>
<text>Reset TUI</text>
</box>
<box onMouseUp={props.onExit} backgroundColor="#565f89" padding={1}>
<text>Exit</text>
</box>
</box>
<scrollbox height={Math.floor(term().height * 0.7)}>
<text>{props.error.stack}</text>
</scrollbox>
<text>{props.error.message}</text>
</box>
)
}
@@ -28,7 +28,9 @@ function init() {
return registrations().flatMap((x) => x()) return registrations().flatMap((x) => x())
}) })
let keybinds = true
useKeyboard((evt) => { useKeyboard((evt) => {
if (!keybinds) return
for (const option of options()) { for (const option of options()) {
if (option.keybind && keybind.match(option.keybind, evt)) { if (option.keybind && keybind.match(option.keybind, evt)) {
evt.preventDefault() evt.preventDefault()
@@ -39,14 +41,20 @@ function init() {
}) })
const result = { const result = {
trigger(name: string) { trigger(name: string, source?: "prompt") {
for (const option of options()) { for (const option of options()) {
if (option.value === name) { if (option.value === name) {
option.onSelect?.(dialog) option.onSelect?.(dialog, source)
return return
} }
} }
}, },
keybinds(enabled: boolean) {
keybinds = enabled
},
show() {
dialog.replace(() => <DialogCommand options={options()} />)
},
register(cb: () => CommandOption[]) { register(cb: () => CommandOption[]) {
const results = createMemo(cb) const results = createMemo(cb)
setRegistrations((arr) => [results, ...arr]) setRegistrations((arr) => [results, ...arr])
@@ -75,7 +83,7 @@ export function CommandProvider(props: ParentProps) {
const keybind = useKeybind() const keybind = useKeybind()
useKeyboard((evt) => { useKeyboard((evt) => {
if (keybind.match("command_list", evt)) { if (keybind.match("command_list", evt) && dialog.stack.length === 0) {
evt.preventDefault() evt.preventDefault()
dialog.replace(() => <DialogCommand options={value.options} />) dialog.replace(() => <DialogCommand options={value.options} />)
return return
@@ -90,7 +98,10 @@ function DialogCommand(props: { options: CommandOption[] }) {
return ( return (
<DialogSelect <DialogSelect
title="Commands" title="Commands"
options={props.options.map((x) => ({ ...x, footer: x.keybind ? keybind.print(x.keybind) : undefined }))} options={props.options.map((x) => ({
...x,
footer: x.keybind ? keybind.print(x.keybind) : undefined,
}))}
/> />
) )
} }
@@ -7,6 +7,7 @@ import { Locale } from "@/util/locale"
import { Keybind } from "@/util/keybind" import { Keybind } from "@/util/keybind"
import { useTheme } from "../context/theme" import { useTheme } from "../context/theme"
import { useSDK } from "../context/sdk" import { useSDK } from "../context/sdk"
import { DialogSessionRename } from "./dialog-session-rename"
export function DialogSessionList() { export function DialogSessionList() {
const dialog = useDialog() const dialog = useDialog()
@@ -59,7 +60,7 @@ export function DialogSessionList() {
}} }}
keybind={[ keybind={[
{ {
keybind: Keybind.parse("delete")[0], keybind: Keybind.parse("ctrl+d")[0],
title: "delete", title: "delete",
onTrigger: async (option) => { onTrigger: async (option) => {
if (toDelete() === option.value) { if (toDelete() === option.value) {
@@ -74,6 +75,13 @@ export function DialogSessionList() {
setToDelete(option.value) setToDelete(option.value)
}, },
}, },
{
keybind: Keybind.parse("ctrl+r")[0],
title: "rename",
onTrigger: async (option) => {
dialog.replace(() => <DialogSessionRename session={option.value} />)
},
},
]} ]}
/> />
) )
@@ -0,0 +1,35 @@
import { DialogPrompt } from "@tui/ui/dialog-prompt"
import { useDialog } from "@tui/ui/dialog"
import { useSync } from "@tui/context/sync"
import { createMemo } from "solid-js"
import { useSDK } from "../context/sdk"
interface DialogSessionRenameProps {
session: string
}
export function DialogSessionRename(props: DialogSessionRenameProps) {
const dialog = useDialog()
const sync = useSync()
const sdk = useSDK()
const session = createMemo(() => sync.session.get(props.session))
return (
<DialogPrompt
title="Rename Session"
value={session()?.title}
onConfirm={(value) => {
sdk.client.session.update({
path: {
id: props.session,
},
body: {
title: value,
},
})
dialog.clear()
}}
onCancel={() => dialog.clear()}
/>
)
}
@@ -1,7 +1,7 @@
import { TextAttributes } from "@opentui/core" import { TextAttributes } from "@opentui/core"
import { useTheme } from "../context/theme" import { useTheme } from "../context/theme"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { For, Match, Switch, Show } from "solid-js" import { For, Match, Switch, Show, createMemo } from "solid-js"
export type DialogStatusProps = {} export type DialogStatusProps = {}
@@ -9,15 +9,19 @@ export function DialogStatus() {
const sync = useSync() const sync = useSync()
const { theme } = useTheme() const { theme } = useTheme()
const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))
return ( return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}> <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD}>Status</text> <text fg={theme.text} attributes={TextAttributes.BOLD}>
Status
</text>
<text fg={theme.textMuted}>esc</text> <text fg={theme.textMuted}>esc</text>
</box> </box>
<Show when={Object.keys(sync.data.mcp).length > 0}> <Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text>No MCP Servers</text>}>
<box> <box>
<text>{Object.keys(sync.data.mcp).length} MCP Servers</text> <text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
<For each={Object.entries(sync.data.mcp)}> <For each={Object.entries(sync.data.mcp)}>
{([key, item]) => ( {([key, item]) => (
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
@@ -33,7 +37,7 @@ export function DialogStatus() {
> >
</text> </text>
<text wrapMode="word"> <text fg={theme.text} wrapMode="word">
<b>{key}</b>{" "} <b>{key}</b>{" "}
<span style={{ fg: theme.textMuted }}> <span style={{ fg: theme.textMuted }}>
<Switch> <Switch>
@@ -50,7 +54,7 @@ export function DialogStatus() {
</Show> </Show>
{sync.data.lsp.length > 0 && ( {sync.data.lsp.length > 0 && (
<box> <box>
<text>{sync.data.lsp.length} LSP Servers</text> <text fg={theme.text}>{sync.data.lsp.length} LSP Servers</text>
<For each={sync.data.lsp}> <For each={sync.data.lsp}>
{(item) => ( {(item) => (
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
@@ -65,7 +69,7 @@ export function DialogStatus() {
> >
</text> </text>
<text wrapMode="word"> <text fg={theme.text} wrapMode="word">
<b>{item.id}</b> <span style={{ fg: theme.textMuted }}>{item.root}</span> <b>{item.id}</b> <span style={{ fg: theme.textMuted }}>{item.root}</span>
</text> </text>
</box> </box>
@@ -73,6 +77,31 @@ export function DialogStatus() {
</For> </For>
</box> </box>
)} )}
<Show
when={enabledFormatters().length > 0}
fallback={<text fg={theme.text}>No Formatters</text>}
>
<box>
<text fg={theme.text}>{enabledFormatters().length} Formatters</text>
<For each={enabledFormatters()}>
{(item) => (
<box flexDirection="row" gap={1}>
<text
flexShrink={0}
style={{
fg: theme.success,
}}
>
</text>
<text wrapMode="word" fg={theme.text}>
<b>{item.name}</b>
</text>
</box>
)}
</For>
</box>
</Show>
</box> </box>
) )
} }
@@ -12,15 +12,16 @@ export function DialogThemeList() {
const dialog = useDialog() const dialog = useDialog()
let confirmed = false let confirmed = false
let ref: DialogSelectRef<keyof typeof THEMES> let ref: DialogSelectRef<keyof typeof THEMES>
const initial = theme.selectedTheme const initial = theme.selected
onMount(() => { onMount(() => {
// highlight the first theme in the list when we open it for UX // highlight the first theme in the list when we open it for UX
theme.setSelectedTheme(Object.keys(THEMES)[0] as keyof typeof THEMES) theme.set(Object.keys(THEMES)[0] as keyof typeof THEMES)
}) })
onCleanup(() => { onCleanup(() => {
// if we close the dialog without confirming, reset back to the initial theme // if we close the dialog without confirming, reset back to the initial theme
if (!confirmed) theme.setSelectedTheme(initial) if (!confirmed) theme.set(initial)
}) })
return ( return (
@@ -28,10 +29,10 @@ export function DialogThemeList() {
title="Themes" title="Themes"
options={options} options={options}
onMove={(opt) => { onMove={(opt) => {
theme.setSelectedTheme(opt.value) theme.set(opt.value)
}} }}
onSelect={(opt) => { onSelect={(opt) => {
theme.setSelectedTheme(opt.value) theme.set(opt.value)
confirmed = true confirmed = true
dialog.clear() dialog.clear()
}} }}
@@ -40,12 +41,12 @@ export function DialogThemeList() {
}} }}
onFilter={(query) => { onFilter={(query) => {
if (query.length === 0) { if (query.length === 0) {
theme.setSelectedTheme(initial) theme.set(initial)
return return
} }
const first = ref.filtered[0] const first = ref.filtered[0]
if (first) theme.setSelectedTheme(first.value) if (first) theme.set(first.value)
}} }}
/> />
) )
@@ -18,6 +18,7 @@ export type AutocompleteRef = {
export type AutocompleteOption = { export type AutocompleteOption = {
display: string display: string
aliases?: string[]
disabled?: boolean disabled?: boolean
description?: string description?: string
onSelect?: () => void onSelect?: () => void
@@ -207,6 +208,7 @@ export function Autocomplete(props: {
}, },
{ {
display: "/compact", display: "/compact",
aliases: ["/summarize"],
description: "compact the session", description: "compact the session",
onSelect: () => command.trigger("session.compact"), onSelect: () => command.trigger("session.compact"),
}, },
@@ -222,11 +224,22 @@ export function Autocomplete(props: {
description: "unshare a session", description: "unshare a session",
onSelect: () => command.trigger("session.unshare"), onSelect: () => command.trigger("session.unshare"),
}, },
{
display: "/rename",
description: "rename session",
onSelect: () => command.trigger("session.rename"),
},
{
display: "/timeline",
description: "jump to message",
onSelect: () => command.trigger("session.timeline"),
},
) )
} }
results.push( results.push(
{ {
display: "/new", display: "/new",
aliases: ["/clear"],
description: "create a new session", description: "create a new session",
onSelect: () => command.trigger("session.new"), onSelect: () => command.trigger("session.new"),
}, },
@@ -242,6 +255,7 @@ export function Autocomplete(props: {
}, },
{ {
display: "/session", display: "/session",
aliases: ["/resume", "/continue"],
description: "list sessions", description: "list sessions",
onSelect: () => command.trigger("session.list"), onSelect: () => command.trigger("session.list"),
}, },
@@ -250,11 +264,32 @@ export function Autocomplete(props: {
description: "show status", description: "show status",
onSelect: () => command.trigger("opencode.status"), onSelect: () => command.trigger("opencode.status"),
}, },
{
display: "/theme",
description: "toggle theme",
onSelect: () => command.trigger("theme.switch"),
},
{
display: "/editor",
description: "open editor",
onSelect: () => command.trigger("prompt.editor", "prompt"),
},
{ {
display: "/help", display: "/help",
description: "show help", description: "show help",
onSelect: () => command.trigger("help.show"), onSelect: () => command.trigger("help.show"),
}, },
{
display: "/commands",
description: "show all commands",
onSelect: () => command.show(),
},
{
display: "/exit",
aliases: ["/quit", "/q"],
description: "exit the app",
onSelect: () => command.trigger("app.exit"),
},
) )
const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length
if (!max) return results if (!max) return results
@@ -273,7 +308,7 @@ export function Autocomplete(props: {
const currentFilter = filter() const currentFilter = filter()
if (!currentFilter) return mixed.slice(0, 10) if (!currentFilter) return mixed.slice(0, 10)
const result = fuzzysort.go(currentFilter, mixed, { const result = fuzzysort.go(currentFilter, mixed, {
keys: ["display", "description"], keys: ["display", "description", (obj) => obj.aliases?.join(" ") ?? ""],
limit: 10, limit: 10,
}) })
return result.map((arr) => arr.obj) return result.map((arr) => arr.obj)
@@ -301,6 +336,7 @@ export function Autocomplete(props: {
} }
function show(mode: "@" | "/") { function show(mode: "@" | "/") {
command.keybinds(false)
setStore({ setStore({
visible: mode, visible: mode,
index: props.input().visualCursor.offset, index: props.input().visualCursor.offset,
@@ -318,6 +354,7 @@ export function Autocomplete(props: {
const cursor = props.input().logicalCursor const cursor = props.input().logicalCursor
props.input().deleteRange(0, 0, cursor.row, cursor.col) props.input().deleteRange(0, 0, cursor.row, cursor.col)
} }
command.keybinds(true)
setStore("visible", false) setStore("visible", false)
} }
@@ -9,9 +9,9 @@ import {
dim, dim,
fg, fg,
} from "@opentui/core" } from "@opentui/core"
import { createEffect, createMemo, Match, Switch, type JSX, onMount } from "solid-js" import { createEffect, createMemo, Match, Switch, type JSX, onMount, batch } from "solid-js"
import { useLocal } from "@tui/context/local" import { useLocal } from "@tui/context/local"
import { SyntaxTheme, useTheme } from "@tui/context/theme" import { useTheme } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border" import { SplitBorder } from "@tui/component/border"
import { useSDK } from "@tui/context/sdk" import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route" import { useRoute } from "@tui/context/route"
@@ -60,7 +60,7 @@ export function Prompt(props: PromptProps) {
const history = usePromptHistory() const history = usePromptHistory()
const command = useCommandDialog() const command = useCommandDialog()
const renderer = useRenderer() const renderer = useRenderer()
const { theme } = useTheme() const { theme, syntax } = useTheme()
const textareaKeybindings = createMemo(() => { const textareaKeybindings = createMemo(() => {
const newlineBindings = keybind.all.input_newline || [] const newlineBindings = keybind.all.input_newline || []
@@ -86,9 +86,9 @@ export function Prompt(props: PromptProps) {
] ]
}) })
const fileStyleId = SyntaxTheme.getStyleId("extmark.file")! const fileStyleId = syntax().getStyleId("extmark.file")!
const agentStyleId = SyntaxTheme.getStyleId("extmark.agent")! const agentStyleId = syntax().getStyleId("extmark.agent")!
const pasteStyleId = SyntaxTheme.getStyleId("extmark.paste")! const pasteStyleId = syntax().getStyleId("extmark.paste")!
let promptPartTypeId: number let promptPartTypeId: number
command.register(() => { command.register(() => {
@@ -98,14 +98,9 @@ export function Prompt(props: PromptProps) {
category: "Session", category: "Session",
keybind: "editor_open", keybind: "editor_open",
value: "prompt.editor", value: "prompt.editor",
onSelect: async (dialog) => { onSelect: async (dialog, trigger) => {
dialog.clear() dialog.clear()
const value = input.plainText const value = trigger === "prompt" ? "" : input.plainText
input.clear()
setStore("prompt", {
input: "",
parts: [],
})
const content = await Editor.open({ value, renderer }) const content = await Editor.open({ value, renderer })
if (content) { if (content) {
input.setText(content, { history: false }) input.setText(content, { history: false })
@@ -139,6 +134,7 @@ export function Prompt(props: PromptProps) {
keybind: "input_submit", keybind: "input_submit",
category: "Prompt", category: "Prompt",
onSelect: (dialog) => { onSelect: (dialog) => {
if (!input.focused) return
submit() submit()
dialog.clear() dialog.clear()
}, },
@@ -194,6 +190,16 @@ export function Prompt(props: PromptProps) {
input.focus() input.focus()
}) })
local.setInitialPrompt.listen((initialPrompt) => {
batch(() => {
setStore("prompt", {
input: initialPrompt,
parts: [],
})
input.insertText(initialPrompt)
})
})
onMount(() => { onMount(() => {
promptPartTypeId = input.extmarks.registerType("prompt-part") promptPartTypeId = input.extmarks.registerType("prompt-part")
}) })
@@ -530,6 +536,17 @@ export function Prompt(props: PromptProps) {
setStore("extmarkToPartIndex", new Map()) setStore("extmarkToPartIndex", new Map())
return return
} }
if (keybind.match("input_forward_delete", e) && store.prompt.input !== "") {
const cursorOffset = input.cursorOffset
if (cursorOffset < input.plainText.length) {
const text = input.plainText
const newText = text.slice(0, cursorOffset) + text.slice(cursorOffset + 1)
input.setText(newText)
input.cursorOffset = cursorOffset
}
e.preventDefault()
return
}
if (keybind.match("app_exit", e)) { if (keybind.match("app_exit", e)) {
await exit() await exit()
return return
@@ -552,10 +569,11 @@ export function Prompt(props: PromptProps) {
if (store.mode === "normal") autocomplete.onKeyDown(e) if (store.mode === "normal") autocomplete.onKeyDown(e)
if (!autocomplete.visible) { if (!autocomplete.visible) {
if ( if (
(e.name === "up" && input.cursorOffset === 0) || (keybind.match("history_previous", e) && input.cursorOffset === 0) ||
(e.name === "down" && input.cursorOffset === input.plainText.length) (keybind.match("history_next", e) &&
input.cursorOffset === input.plainText.length)
) { ) {
const direction = e.name === "up" ? -1 : 1 const direction = keybind.match("history_previous", e) ? -1 : 1
const item = history.move(direction, input.plainText) const item = history.move(direction, input.plainText)
if (item) { if (item) {
@@ -599,14 +617,16 @@ export function Prompt(props: PromptProps) {
// trim ' from the beginning and end of the pasted content. just // trim ' from the beginning and end of the pasted content. just
// ' and nothing else // ' and nothing else
const filepath = pastedContent.replace(/^'+|'+$/g, "") const filepath = pastedContent.replace(/^'+|'+$/g, "").replace(/\\ /g, " ")
console.log(pastedContent, filepath)
try { try {
const file = Bun.file(filepath) const file = Bun.file(filepath)
if (file.type.startsWith("image/")) { if (file.type.startsWith("image/")) {
event.preventDefault()
const content = await file const content = await file
.arrayBuffer() .arrayBuffer()
.then((buffer) => Buffer.from(buffer).toString("base64")) .then((buffer) => Buffer.from(buffer).toString("base64"))
.catch(() => {}) .catch(console.error)
if (content) { if (content) {
await pasteImage({ await pasteImage({
filename: file.name, filename: file.name,
@@ -663,7 +683,7 @@ export function Prompt(props: PromptProps) {
onMouseDown={(r: MouseEvent) => r.target?.focus()} onMouseDown={(r: MouseEvent) => r.target?.focus()}
focusedBackgroundColor={theme.backgroundElement} focusedBackgroundColor={theme.backgroundElement}
cursorColor={theme.primary} cursorColor={theme.primary}
syntaxStyle={SyntaxTheme} syntaxStyle={syntax()}
/> />
</box> </box>
<box <box
@@ -674,7 +694,7 @@ export function Prompt(props: PromptProps) {
></box> ></box>
</box> </box>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text flexShrink={0} wrapMode="none"> <text flexShrink={0} wrapMode="none" fg={theme.text}>
<span style={{ fg: theme.textMuted }}>{local.model.parsed().provider}</span>{" "} <span style={{ fg: theme.textMuted }}>{local.model.parsed().provider}</span>{" "}
<span style={{ bold: true }}>{local.model.parsed().model}</span> <span style={{ bold: true }}>{local.model.parsed().model}</span>
</text> </text>
@@ -684,14 +704,14 @@ export function Prompt(props: PromptProps) {
</Match> </Match>
<Match when={status() === "working"}> <Match when={status() === "working"}>
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
<text> <text fg={theme.text}>
esc <span style={{ fg: theme.textMuted }}>interrupt</span> esc <span style={{ fg: theme.textMuted }}>interrupt</span>
</text> </text>
</box> </box>
</Match> </Match>
<Match when={props.hint}>{props.hint!}</Match> <Match when={props.hint}>{props.hint!}</Match>
<Match when={true}> <Match when={true}>
<text> <text fg={theme.text}>
ctrl+p <span style={{ fg: theme.textMuted }}>commands</span> ctrl+p <span style={{ fg: theme.textMuted }}>commands</span>
</text> </text>
</Match> </Match>
@@ -1,5 +1,5 @@
import { Global } from "@/global" import { Global } from "@/global"
import { createSignal } from "solid-js" import { createSignal, type Setter } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { createSimpleContext } from "./helper" import { createSimpleContext } from "./helper"
import path from "path" import path from "path"
@@ -8,10 +8,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
name: "KV", name: "KV",
init: () => { init: () => {
const [ready, setReady] = createSignal(false) const [ready, setReady] = createSignal(false)
const [kvStore, setKvStore] = createStore({ const [kvStore, setKvStore] = createStore<Record<string, any>>()
openrouter_warning: false,
theme: "opencode",
})
const file = Bun.file(path.join(Global.Path.state, "kv.json")) const file = Bun.file(path.join(Global.Path.state, "kv.json"))
file file
@@ -24,22 +21,29 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
setReady(true) setReady(true)
}) })
return { const result = {
get data() {
return kvStore
},
get ready() { get ready() {
return ready() return ready()
}, },
signal<T>(name: string, defaultValue: T) {
if (!kvStore[name]) setKvStore(name, defaultValue)
return [
function () {
return result.get(name)
},
function setter(next: Setter<T>) {
result.set(name, next)
},
] as const
},
get(key: string, defaultValue?: any) {
return kvStore[key] ?? defaultValue
},
set(key: string, value: any) { set(key: string, value: any) {
setKvStore(key as any, value) setKvStore(key, value)
Bun.write( Bun.write(file, JSON.stringify(kvStore, null, 2))
file,
JSON.stringify({
[key]: value,
}),
)
}, },
} }
return result
}, },
}) })
@@ -8,10 +8,11 @@ import { Global } from "@/global"
import { iife } from "@/util/iife" import { iife } from "@/util/iife"
import { createSimpleContext } from "./helper" import { createSimpleContext } from "./helper"
import { useToast } from "../ui/toast" import { useToast } from "../ui/toast"
import { createEventBus } from "@solid-primitives/event-bus"
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local", name: "Local",
init: (props: { initialModel?: string; initialAgent?: string }) => { init: (props: { initialModel?: string; initialAgent?: string; initialPrompt?: string }) => {
const sync = useSync() const sync = useSync()
const toast = useToast() const toast = useToast()
@@ -147,15 +148,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
setModelStore("ready", true) setModelStore("ready", true)
}) })
createEffect(() => {
Bun.write(
file,
JSON.stringify({
recent: modelStore.recent,
}),
)
})
const fallbackModel = createMemo(() => { const fallbackModel = createMemo(() => {
if (sync.data.config.model) { if (sync.data.config.model) {
const [providerID, modelID] = sync.data.config.model.split("/") const [providerID, modelID] = sync.data.config.model.split("/")
@@ -206,6 +198,21 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
model: model.name ?? value.modelID, model: model.name ?? value.modelID,
} }
}), }),
cycle(direction: 1 | -1) {
const current = currentModel()
if (!current) return
const recent = modelStore.recent
const index = recent.findIndex(
(x) => x.providerID === current.providerID && x.modelID === current.modelID,
)
if (index === -1) return
let next = index + direction
if (next < 0) next = recent.length - 1
if (next >= recent.length) next = 0
const val = recent[next]
if (!val) return
setModelStore("model", agent.current().name, { ...val })
},
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) { set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
batch(() => { batch(() => {
if (!isModelValid(model)) { if (!isModelValid(model)) {
@@ -216,21 +223,36 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}) })
return return
} }
setModelStore("model", agent.current().name, model) setModelStore("model", agent.current().name, model)
if (options?.recent) { if (options?.recent) {
const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID) const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID)
if (uniq.length > 5) uniq.pop() if (uniq.length > 5) uniq.pop()
setModelStore("recent", uniq) setModelStore("recent", uniq)
Bun.write(
file,
JSON.stringify({
recent: modelStore.recent,
}),
)
} }
}) })
}, },
} }
}) })
const setInitialPrompt = createEventBus<string>()
onMount(() => {
if (props.initialPrompt)
setInitialPrompt.emit(props.initialPrompt)
})
const result = { const result = {
model, model,
agent, agent,
get setInitialPrompt() {
return setInitialPrompt
},
} }
return result return result
}, },
@@ -10,6 +10,7 @@ import type {
Permission, Permission,
LspStatus, LspStatus,
McpStatus, McpStatus,
FormatterStatus,
} from "@opencode-ai/sdk" } from "@opencode-ai/sdk"
import { createStore, produce, reconcile } from "solid-js/store" import { createStore, produce, reconcile } from "solid-js/store"
import { useSDK } from "@tui/context/sdk" import { useSDK } from "@tui/context/sdk"
@@ -42,6 +43,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
mcp: { mcp: {
[key: string]: McpStatus [key: string]: McpStatus
} }
formatter: FormatterStatus[]
}>({ }>({
config: {}, config: {},
ready: false, ready: false,
@@ -55,6 +57,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
part: {}, part: {},
lsp: [], lsp: [],
mcp: {}, mcp: {},
formatter: [],
}) })
const sdk = useSDK() const sdk = useSDK()
@@ -220,6 +223,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
sdk.client.command.list().then((x) => setStore("command", x.data ?? [])), sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)), sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)), sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
]) ])
const result = { const result = {
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
import { Prompt, type PromptRef } from "@tui/component/prompt" import { Prompt } from "@tui/component/prompt"
import { createEffect, createMemo, Match, Show, Switch, type ParentProps } from "solid-js" import { createMemo, Match, Show, Switch, type ParentProps } from "solid-js"
import { useTheme } from "@tui/context/theme" import { useTheme } from "@tui/context/theme"
import { useKeybind } from "../context/keybind" import { useKeybind } from "../context/keybind"
import type { KeybindsConfig } from "@opencode-ai/sdk" import type { KeybindsConfig } from "@opencode-ai/sdk"
@@ -7,27 +7,18 @@ import { Logo } from "../component/logo"
import { Locale } from "@/util/locale" import { Locale } from "@/util/locale"
import { useSync } from "../context/sync" import { useSync } from "../context/sync"
import { Toast } from "../ui/toast" import { Toast } from "../ui/toast"
import { useDialog } from "../ui/dialog"
export function Home() { export function Home() {
const sync = useSync() const sync = useSync()
const { theme } = useTheme() const { theme } = useTheme()
const dialog = useDialog()
const mcpError = createMemo(() => { const mcpError = createMemo(() => {
return Object.values(sync.data.mcp).some((x) => x.status === "failed") return Object.values(sync.data.mcp).some((x) => x.status === "failed")
}) })
let promptRef: PromptRef | undefined = undefined
createEffect(() => {
dialog.allClosedEvent.listen(() => {
promptRef?.focus()
})
})
const Hint = ( const Hint = (
<Show when={Object.keys(sync.data.mcp).length > 0}> <Show when={Object.keys(sync.data.mcp).length > 0}>
<box flexShrink={0} flexDirection="row" gap={1}> <box flexShrink={0} flexDirection="row" gap={1}>
<text> <text fg={theme.text}>
<Switch> <Switch>
<Match when={mcpError()}> <Match when={mcpError()}>
<span style={{ fg: theme.error }}></span> mcp errors{" "} <span style={{ fg: theme.error }}></span> mcp errors{" "}
@@ -64,7 +55,7 @@ export function Home() {
<HelpRow keybind="agent_cycle">Switch agent</HelpRow> <HelpRow keybind="agent_cycle">Switch agent</HelpRow>
</box> </box>
<box width="100%" maxWidth={75} zIndex={1000} paddingTop={1}> <box width="100%" maxWidth={75} zIndex={1000} paddingTop={1}>
<Prompt hint={Hint} ref={(r) => (promptRef = r)} /> <Prompt hint={Hint} />
</box> </box>
<Toast /> <Toast />
</box> </box>
@@ -76,7 +67,7 @@ function HelpRow(props: ParentProps<{ keybind: keyof KeybindsConfig }>) {
const { theme } = useTheme() const { theme } = useTheme()
return ( return (
<box flexDirection="row" justifyContent="space-between" width="100%"> <box flexDirection="row" justifyContent="space-between" width="100%">
<text>{props.children}</text> <text fg={theme.text}>{props.children}</text>
<text fg={theme.primary}>{keybind.print(props.keybind)}</text> <text fg={theme.primary}>{keybind.print(props.keybind)}</text>
</box> </box>
) )
@@ -51,7 +51,7 @@ export function Header() {
borderColor={theme.backgroundElement} borderColor={theme.backgroundElement}
flexShrink={0} flexShrink={0}
> >
<text> <text fg={theme.text}>
<span style={{ bold: true, fg: theme.accent }}>#</span>{" "} <span style={{ bold: true, fg: theme.accent }}>#</span>{" "}
<span style={{ bold: true }}>{session().title}</span> <span style={{ bold: true }}>{session().title}</span>
</text> </text>
@@ -64,7 +64,7 @@ export function Header() {
</text> </text>
</Match> </Match>
<Match when={true}> <Match when={true}>
<text wrapMode="word"> <text fg={theme.text} wrapMode="word">
/share <span style={{ fg: theme.textMuted }}>to create a shareable link</span> /share <span style={{ fg: theme.textMuted }}>to create a shareable link</span>
</text> </text>
</Match> </Match>
@@ -12,10 +12,10 @@ import {
} from "solid-js" } from "solid-js"
import { Dynamic } from "solid-js/web" import { Dynamic } from "solid-js/web"
import path from "path" import path from "path"
import { useRouteData } from "@tui/context/route" import { useRoute, useRouteData } from "@tui/context/route"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { SplitBorder } from "@tui/component/border" import { SplitBorder } from "@tui/component/border"
import { SyntaxTheme, useTheme } from "@tui/context/theme" import { useTheme } from "@tui/context/theme"
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core" import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core"
import { Prompt, type PromptRef } from "@tui/component/prompt" import { Prompt, type PromptRef } from "@tui/component/prompt"
import type { import type {
@@ -62,7 +62,10 @@ import { DialogTimeline } from "./dialog-timeline"
import { Sidebar } from "./sidebar" import { Sidebar } from "./sidebar"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
import parsers from "../../../../../../parsers-config.ts" import parsers from "../../../../../../parsers-config.ts"
import { Toast } from "../../ui/toast" import { Clipboard } from "../../util/clipboard"
import { Toast, useToast } from "../../ui/toast"
import { DialogSessionRename } from "../../component/dialog-session-rename"
import { useKV } from "../../context/kv.tsx"
addDefaultParsers(parsers.parsers) addDefaultParsers(parsers.parsers)
@@ -79,7 +82,9 @@ function use() {
export function Session() { export function Session() {
const route = useRouteData("session") const route = useRouteData("session")
const { navigate } = useRoute()
const sync = useSync() const sync = useSync()
const kv = useKV()
const { theme } = useTheme() const { theme } = useTheme()
const session = createMemo(() => sync.session.get(route.sessionID)!) const session = createMemo(() => sync.session.get(route.sessionID)!)
const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
@@ -90,7 +95,7 @@ export function Session() {
}) })
const dimensions = useTerminalDimensions() const dimensions = useTerminalDimensions()
const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">("auto") const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">(kv.get("sidebar", "auto"))
const [conceal, setConceal] = createSignal(true) const [conceal, setConceal] = createSignal(true)
const wide = createMemo(() => dimensions().width > 120) const wide = createMemo(() => dimensions().width > 120)
@@ -99,18 +104,14 @@ export function Session() {
createEffect(() => sync.session.sync(route.sessionID)) createEffect(() => sync.session.sync(route.sessionID))
const toast = useToast()
const sdk = useSDK() const sdk = useSDK()
let scroll: ScrollBoxRenderable let scroll: ScrollBoxRenderable
let prompt: PromptRef let prompt: PromptRef
const keybind = useKeybind() const keybind = useKeybind()
createEffect(() => {
dialog.allClosedEvent.listen(() => {
prompt.focus()
})
})
useKeyboard((evt) => { useKeyboard((evt) => {
if (dialog.stack.length > 0) return if (dialog.stack.length > 0) return
@@ -150,6 +151,23 @@ export function Session() {
const local = useLocal() const local = useLocal()
function moveChild(direction: number) {
const parentID = session()?.parentID ?? session()?.id
let children = sync.data.session
.filter((x) => x.parentID === parentID || x.id === parentID)
.toSorted((b, a) => a.id.localeCompare(b.id))
if (children.length === 1) return
let next = children.findIndex((x) => x.id === session()?.id) + direction
if (next >= children.length) next = 0
if (next < 0) next = children.length - 1
if (children[next]) {
navigate({
type: "session",
sessionID: children[next].id,
})
}
}
const command = useCommandDialog() const command = useCommandDialog()
command.register(() => [ command.register(() => [
{ {
@@ -195,12 +213,20 @@ export function Session() {
keybind: "session_share", keybind: "session_share",
disabled: !!session()?.share?.url, disabled: !!session()?.share?.url,
category: "Session", category: "Session",
onSelect: (dialog) => { onSelect: async (dialog) => {
sdk.client.session.share({ await sdk.client.session
path: { .share({
id: route.sessionID, path: {
}, id: route.sessionID,
}) },
})
.then((res) =>
Clipboard.copy(res.data!.share!.url).catch(() =>
toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }),
),
)
.then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
.catch(() => toast.show({ message: "Failed to share session", variant: "error" }))
dialog.clear() dialog.clear()
}, },
}, },
@@ -240,7 +266,9 @@ export function Session() {
prompt.set( prompt.set(
parts.reduce( parts.reduce(
(agg, part) => { (agg, part) => {
if (part.type === "text") agg.input += part.text if (part.type === "text") {
if (!part.synthetic) agg.input += part.text
}
if (part.type === "file") agg.parts.push(part) if (part.type === "file") agg.parts.push(part)
return agg return agg
}, },
@@ -291,6 +319,8 @@ export function Session() {
if (prev === "show") return "hide" if (prev === "show") return "hide"
return "show" return "show"
}) })
if (sidebar() === "show") kv.set("sidebar", "auto")
if (sidebar() === "hide") kv.set("sidebar", "hide")
dialog.clear() dialog.clear()
}, },
}, },
@@ -370,6 +400,37 @@ export function Session() {
dialog.clear() dialog.clear()
}, },
}, },
{
title: "Rename session",
value: "session.rename",
keybind: "session_rename",
category: "Session",
onSelect: (dialog) => {
dialog.replace(() => <DialogSessionRename session={route.sessionID} />)
},
},
{
title: "Next child session",
value: "session.child.next",
keybind: "session_child_cycle",
category: "Session",
disabled: true,
onSelect: (dialog) => {
moveChild(1)
dialog.clear()
},
},
{
title: "Previous child session",
value: "session.child.previous",
keybind: "session_child_cycle_reverse",
category: "Session",
disabled: true,
onSelect: (dialog) => {
moveChild(-1)
dialog.clear()
},
},
]) ])
const revert = createMemo(() => { const revert = createMemo(() => {
@@ -431,6 +492,34 @@ export function Session() {
> >
<box flexGrow={1} gap={1}> <box flexGrow={1} gap={1}>
<Show when={session()}> <Show when={session()}>
<Show when={session().parentID}>
<box
backgroundColor={theme.backgroundPanel}
justifyContent="space-between"
flexDirection="row"
paddingTop={1}
paddingBottom={1}
flexShrink={0}
paddingLeft={2}
paddingRight={2}
>
<text fg={theme.text}>
Previous{" "}
<span style={{ fg: theme.textMuted }}>
{keybind.print("session_child_cycle_reverse")}
</span>
</text>
<text fg={theme.text}>
<b>Viewing subagent session</b>
</text>
<text fg={theme.text}>
<span style={{ fg: theme.textMuted }}>
{keybind.print("session_child_cycle")}
</span>{" "}
Next
</text>
</box>
</Show>
<Show when={!sidebarVisible()}> <Show when={!sidebarVisible()}>
<Header /> <Header />
</Show> </Show>
@@ -614,7 +703,7 @@ function UserMessage(props: {
borderColor={color()} borderColor={color()}
flexShrink={0} flexShrink={0}
> >
<text>{text()?.text}</text> <text fg={theme.text}>{text()?.text}</text>
<Show when={files().length}> <Show when={files().length}>
<box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap"> <box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap">
<For each={files()}> <For each={files()}>
@@ -625,7 +714,7 @@ function UserMessage(props: {
return theme.secondary return theme.secondary
}) })
return ( return (
<text> <text fg={theme.text}>
<span style={{ bg: bg(), fg: theme.background }}> <span style={{ bg: bg(), fg: theme.background }}>
{" "} {" "}
{MIME_BADGE[file.mime] ?? file.mime}{" "} {MIME_BADGE[file.mime] ?? file.mime}{" "}
@@ -640,7 +729,7 @@ function UserMessage(props: {
</For> </For>
</box> </box>
</Show> </Show>
<text> <text fg={theme.text}>
{sync.data.config.username ?? "You"}{" "} {sync.data.config.username ?? "You"}{" "}
<Show <Show
when={queued()} when={queued()}
@@ -755,7 +844,7 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }
paddingLeft={2} paddingLeft={2}
backgroundColor={theme.backgroundPanel} backgroundColor={theme.backgroundPanel}
> >
<text>{props.part.text.trim()}</text> <text fg={theme.text}>{props.part.text.trim()}</text>
</box> </box>
</box> </box>
</Show> </Show>
@@ -764,13 +853,14 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }
function TextPart(props: { part: TextPart; message: AssistantMessage }) { function TextPart(props: { part: TextPart; message: AssistantMessage }) {
const ctx = use() const ctx = use()
const { syntax } = useTheme()
return ( return (
<Show when={props.part.text.trim()}> <Show when={props.part.text.trim()}>
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}> <box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
<code <code
filetype="markdown" filetype="markdown"
drawUnstyledText={false} drawUnstyledText={false}
syntaxStyle={SyntaxTheme} syntaxStyle={syntax()}
content={props.part.text.trim()} content={props.part.text.trim()}
conceal={ctx.conceal()} conceal={ctx.conceal()}
/> />
@@ -893,16 +983,14 @@ function GenericTool(props: ToolProps<any>) {
) )
} }
type ToolRegistration<T extends Tool.Info = any> = {
name: string
container: "inline" | "block"
render?: Component<ToolProps<T>>
}
const ToolRegistry = (() => { const ToolRegistry = (() => {
const state: Record< const state: Record<string, ToolRegistration> = {}
string, function register<T extends Tool.Info>(input: ToolRegistration<T>) {
{ name: string; container: "inline" | "block"; render?: Component<ToolProps<any>> }
> = {}
function register<T extends Tool.Info>(input: {
name: string
container: "inline" | "block"
render?: Component<ToolProps<T>>
}) {
state[input.name] = input state[input.name] = input
return input return input
} }
@@ -970,7 +1058,7 @@ ToolRegistry.register<typeof WriteTool>({
name: "write", name: "write",
container: "block", container: "block",
render(props) { render(props) {
const { theme } = useTheme() const { theme, syntax } = useTheme()
const lines = createMemo(() => { const lines = createMemo(() => {
return props.input.content?.split("\n") ?? [] return props.input.content?.split("\n") ?? []
}) })
@@ -1001,7 +1089,7 @@ ToolRegistry.register<typeof WriteTool>({
<box paddingLeft={1} flexGrow={1}> <box paddingLeft={1} flexGrow={1}>
<code <code
filetype={filetype(props.input.filePath!)} filetype={filetype(props.input.filePath!)}
syntaxStyle={SyntaxTheme} syntaxStyle={syntax()}
content={code()} content={code()}
/> />
</box> </box>
@@ -1066,6 +1154,8 @@ ToolRegistry.register<typeof TaskTool>({
container: "block", container: "block",
render(props) { render(props) {
const { theme } = useTheme() const { theme } = useTheme()
const keybind = useKeybind()
return ( return (
<> <>
<ToolTitle icon="%" fallback="Delegating..." when={props.input.description}> <ToolTitle icon="%" fallback="Delegating..." when={props.input.description}>
@@ -1082,6 +1172,10 @@ ToolRegistry.register<typeof TaskTool>({
</For> </For>
</box> </box>
</Show> </Show>
<text fg={theme.text}>
{keybind.print("session_child_cycle")}, {keybind.print("session_child_cycle_reverse")}
<span style={{ fg: theme.textMuted }}> to navigate between subagent sessions</span>
</text>
</> </>
) )
}, },
@@ -1104,6 +1198,7 @@ ToolRegistry.register<typeof EditTool>({
container: "block", container: "block",
render(props) { render(props) {
const ctx = use() const ctx = use()
const { theme, syntax } = useTheme()
const style = createMemo(() => (ctx.width > 120 ? "split" : "stacked")) const style = createMemo(() => (ctx.width > 120 ? "split" : "stacked"))
@@ -1183,21 +1278,21 @@ ToolRegistry.register<typeof EditTool>({
</ToolTitle> </ToolTitle>
<Switch> <Switch>
<Match when={props.permission["diff"]}> <Match when={props.permission["diff"]}>
<text>{props.permission["diff"]?.trim()}</text> <text fg={theme.text}>{props.permission["diff"]?.trim()}</text>
</Match> </Match>
<Match when={diff() && style() === "split"}> <Match when={diff() && style() === "split"}>
<box paddingLeft={1} flexDirection="row" gap={2}> <box paddingLeft={1} flexDirection="row" gap={2}>
<box flexGrow={1} flexBasis={0}> <box flexGrow={1} flexBasis={0}>
<code filetype={ft()} syntaxStyle={SyntaxTheme} content={diff()!.oldContent} /> <code filetype={ft()} syntaxStyle={syntax()} content={diff()!.oldContent} />
</box> </box>
<box flexGrow={1} flexBasis={0}> <box flexGrow={1} flexBasis={0}>
<code filetype={ft()} syntaxStyle={SyntaxTheme} content={diff()!.newContent} /> <code filetype={ft()} syntaxStyle={syntax()} content={diff()!.newContent} />
</box> </box>
</box> </box>
</Match> </Match>
<Match when={code()}> <Match when={code()}>
<box paddingLeft={1}> <box paddingLeft={1}>
<code filetype={ft()} syntaxStyle={SyntaxTheme} content={code()} /> <code filetype={ft()} syntaxStyle={syntax()} content={code()} />
</box> </box>
</Match> </Match>
</Switch> </Switch>
@@ -1210,6 +1305,7 @@ ToolRegistry.register<typeof PatchTool>({
name: "patch", name: "patch",
container: "block", container: "block",
render(props) { render(props) {
const { theme } = useTheme()
return ( return (
<> <>
<ToolTitle icon="%" fallback="Preparing patch..." when={true}> <ToolTitle icon="%" fallback="Preparing patch..." when={true}>
@@ -1217,7 +1313,7 @@ ToolRegistry.register<typeof PatchTool>({
</ToolTitle> </ToolTitle>
<Show when={props.output}> <Show when={props.output}>
<box> <box>
<text>{props.output?.trim()}</text> <text fg={theme.text}>{props.output?.trim()}</text>
</box> </box>
</Show> </Show>
</> </>
@@ -42,7 +42,7 @@ export function Sidebar(props: { sessionID: string }) {
<Show when={session()}> <Show when={session()}>
<box flexShrink={0} gap={1} width={40}> <box flexShrink={0} gap={1} width={40}>
<box> <box>
<text> <text fg={theme.text}>
<b>{session().title}</b> <b>{session().title}</b>
</text> </text>
<Show when={session().share?.url}> <Show when={session().share?.url}>
@@ -50,7 +50,7 @@ export function Sidebar(props: { sessionID: string }) {
</Show> </Show>
</box> </box>
<box> <box>
<text> <text fg={theme.text}>
<b>Context</b> <b>Context</b>
</text> </text>
<text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text> <text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text>
@@ -59,7 +59,7 @@ export function Sidebar(props: { sessionID: string }) {
</box> </box>
<Show when={Object.keys(sync.data.mcp).length > 0}> <Show when={Object.keys(sync.data.mcp).length > 0}>
<box> <box>
<text> <text fg={theme.text}>
<b>MCP</b> <b>MCP</b>
</text> </text>
<For each={Object.entries(sync.data.mcp)}> <For each={Object.entries(sync.data.mcp)}>
@@ -77,7 +77,7 @@ export function Sidebar(props: { sessionID: string }) {
> >
</text> </text>
<text wrapMode="word"> <text fg={theme.text} wrapMode="word">
{key}{" "} {key}{" "}
<span style={{ fg: theme.textMuted }}> <span style={{ fg: theme.textMuted }}>
<Switch> <Switch>
@@ -96,7 +96,7 @@ export function Sidebar(props: { sessionID: string }) {
</Show> </Show>
<Show when={sync.data.lsp.length > 0}> <Show when={sync.data.lsp.length > 0}>
<box> <box>
<text> <text fg={theme.text}>
<b>LSP</b> <b>LSP</b>
</text> </text>
<For each={sync.data.lsp}> <For each={sync.data.lsp}>
@@ -123,7 +123,7 @@ export function Sidebar(props: { sessionID: string }) {
</Show> </Show>
<Show when={session().summary?.diffs}> <Show when={session().summary?.diffs}>
<box> <box>
<text> <text fg={theme.text}>
<b>Modified Files</b> <b>Modified Files</b>
</text> </text>
<For each={session().summary?.diffs || []}> <For each={session().summary?.diffs || []}>
@@ -155,7 +155,7 @@ export function Sidebar(props: { sessionID: string }) {
</Show> </Show>
<Show when={todo().length > 0}> <Show when={todo().length > 0}>
<box> <box>
<text> <text fg={theme.text}>
<b>Todo</b> <b>Todo</b>
</text> </text>
<For each={todo()}> <For each={todo()}>
@@ -18,7 +18,6 @@ export const TuiSpawnCommand = cmd({
default: 0, default: 0,
}) })
.option("hostname", { .option("hostname", {
alias: ["h"],
type: "string", type: "string",
describe: "hostname to listen on", describe: "hostname to listen on",
default: "127.0.0.1", default: "127.0.0.1",
+6 -1
View File
@@ -32,6 +32,11 @@ export const TuiThreadCommand = cmd({
describe: "session id to continue", describe: "session id to continue",
type: "string", type: "string",
}) })
.option("prompt", {
alias: ["p"],
type: "string",
describe: "prompt to use",
})
.option("agent", { .option("agent", {
type: "string", type: "string",
describe: "agent to use", describe: "agent to use",
@@ -42,7 +47,6 @@ export const TuiThreadCommand = cmd({
default: 0, default: 0,
}) })
.option("hostname", { .option("hostname", {
alias: ["h"],
type: "string", type: "string",
describe: "hostname to listen on", describe: "hostname to listen on",
default: "127.0.0.1", default: "127.0.0.1",
@@ -96,6 +100,7 @@ export const TuiThreadCommand = cmd({
sessionID, sessionID,
model: args.model, model: args.model,
agent: args.agent, agent: args.agent,
prompt: args.prompt,
onExit: async () => { onExit: async () => {
await client.call("shutdown", undefined) await client.call("shutdown", undefined)
}, },
@@ -0,0 +1,65 @@
import { TextareaRenderable, TextAttributes } from "@opentui/core"
import { useTheme } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
import { onMount } from "solid-js"
export type DialogPromptProps = {
title: string
value?: string
onConfirm?: (value: string) => void
onCancel?: () => void
}
export function DialogPrompt(props: DialogPromptProps) {
const dialog = useDialog()
const { theme } = useTheme()
let textarea: TextareaRenderable
onMount(() => {
dialog.setSize("large")
setTimeout(() => {
textarea.focus()
}, 1)
textarea.gotoLineEnd()
})
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD}>{props.title}</text>
<text fg={theme.textMuted}>esc</text>
</box>
<box>
<textarea
onSubmit={() => {
props.onConfirm?.(textarea.plainText)
dialog.clear()
}}
keyBindings={[{ name: "return", action: "submit" }]}
ref={(val: TextareaRenderable) => (textarea = val)}
initialValue={props.value}
placeholder="Enter text"
/>
</box>
<box paddingBottom={1}>
<text fg={theme.textMuted}>Press enter to confirm, esc to cancel</text>
</box>
</box>
)
}
DialogPrompt.show = (dialog: DialogContext, title: string, value?: string) => {
return new Promise<string | null>((resolve) => {
dialog.replace(
() => (
<DialogPrompt
title={title}
value={value}
onConfirm={(value) => resolve(value)}
onCancel={() => resolve(null)}
/>
),
() => resolve(null),
)
})
}
@@ -35,7 +35,7 @@ export interface DialogSelectOption<T = any> {
category?: string category?: string
disabled?: boolean disabled?: boolean
bg?: RGBA bg?: RGBA
onSelect?: (ctx: DialogContext) => void onSelect?: (ctx: DialogContext, trigger?: "prompt") => void
} }
export type DialogSelectRef<T> = { export type DialogSelectRef<T> = {
@@ -59,7 +59,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
props.options, props.options,
filter((x) => x.disabled !== true), filter((x) => x.disabled !== true),
take(props.limit ?? Infinity), take(props.limit ?? Infinity),
(x) => (!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj)), (x) =>
!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj),
) )
return result return result
}) })
@@ -122,20 +123,26 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
const keybind = useKeybind() const keybind = useKeybind()
useKeyboard((evt) => { useKeyboard((evt) => {
if (evt.name === "up") move(-1) if (evt.name === "up" || (evt.ctrl && evt.name === "p")) move(-1)
if (evt.name === "down") move(1) if (evt.name === "down" || (evt.ctrl && evt.name === "n")) move(1)
if (evt.name === "pageup") move(-10) if (evt.name === "pageup") move(-10)
if (evt.name === "pagedown") move(10) if (evt.name === "pagedown") move(10)
if (evt.name === "return") { if (evt.name === "return") {
const option = selected() const option = selected()
if (option.onSelect) option.onSelect(dialog) if (option) {
props.onSelect?.(option) // evt.preventDefault()
if (option.onSelect) option.onSelect(dialog)
props.onSelect?.(option)
}
} }
for (const item of props.keybind ?? []) { for (const item of props.keybind ?? []) {
if (Keybind.match(item.keybind, keybind.parse(evt))) { if (Keybind.match(item.keybind, keybind.parse(evt))) {
const s = selected() const s = selected()
if (s) item.onTrigger(s) if (s) {
evt.preventDefault()
item.onTrigger(s)
}
} }
} }
}) })
@@ -155,7 +162,9 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
<box gap={1}> <box gap={1}>
<box paddingLeft={3} paddingRight={2}> <box paddingLeft={3} paddingRight={2}>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD}>{props.title}</text> <text fg={theme.text} attributes={TextAttributes.BOLD}>
{props.title}
</text>
<text fg={theme.textMuted}>esc</text> <text fg={theme.textMuted}>esc</text>
</box> </box>
<box paddingTop={1} paddingBottom={1}> <box paddingTop={1} paddingBottom={1}>
@@ -166,12 +175,13 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
props.onFilter?.(e) props.onFilter?.(e)
}) })
}} }}
onKeyDown={(e) => {}}
focusedBackgroundColor={theme.backgroundPanel} focusedBackgroundColor={theme.backgroundPanel}
cursorColor={theme.primary} cursorColor={theme.primary}
focusedTextColor={theme.textMuted} focusedTextColor={theme.textMuted}
ref={(r) => { ref={(r) => {
input = r input = r
input.focus() setTimeout(() => input.focus(), 1)
}} }}
placeholder="Enter search term" placeholder="Enter search term"
/> />
@@ -206,11 +216,15 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
props.onSelect?.(option) props.onSelect?.(option)
}} }}
onMouseOver={() => { onMouseOver={() => {
const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value)) const index = filtered().findIndex((x) =>
isDeepEqual(x.value, option.value),
)
if (index === -1) return if (index === -1) return
moveTo(index) moveTo(index)
}} }}
backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)} backgroundColor={
active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)
}
paddingLeft={1} paddingLeft={1}
paddingRight={1} paddingRight={1}
gap={1} gap={1}
@@ -218,7 +232,9 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
<Option <Option
title={option.title} title={option.title}
footer={option.footer} footer={option.footer}
description={option.description !== category ? option.description : undefined} description={
option.description !== category ? option.description : undefined
}
active={active()} active={active()}
current={isDeepEqual(option.value, props.current)} current={isDeepEqual(option.value, props.current)}
/> />
@@ -230,11 +246,13 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
)} )}
</For> </For>
</scrollbox> </scrollbox>
<box paddingRight={2} paddingLeft={3} flexDirection="row" paddingBottom={1}> <box paddingRight={2} paddingLeft={3} flexDirection="row" paddingBottom={1} gap={1}>
<For each={props.keybind ?? []}> <For each={props.keybind ?? []}>
{(item) => ( {(item) => (
<text> <text>
<span style={{ fg: theme.text, attributes: TextAttributes.BOLD }}>{Keybind.toString(item.keybind)}</span> <span style={{ fg: theme.text, attributes: TextAttributes.BOLD }}>
{Keybind.toString(item.keybind)}
</span>
<span style={{ fg: theme.textMuted }}> {item.title}</span> <span style={{ fg: theme.textMuted }}> {item.title}</span>
</text> </text>
)} )}
@@ -263,7 +281,10 @@ function Option(props: {
wrapMode="none" wrapMode="none"
> >
{Locale.truncate(props.title, 62)} {Locale.truncate(props.title, 62)}
<span style={{ fg: props.active ? theme.background : theme.textMuted }}> {props.description}</span> <span style={{ fg: props.active ? theme.background : theme.textMuted }}>
{" "}
{props.description}
</span>
</text> </text>
<Show when={props.footer}> <Show when={props.footer}>
<box flexShrink={0}> <box flexShrink={0}>
@@ -1,23 +1,9 @@
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
import { batch, createContext, createEffect, Show, useContext, type JSX, type ParentProps } from "solid-js" import { batch, createContext, Show, useContext, type JSX, type ParentProps } from "solid-js"
import { useTheme } from "@tui/context/theme" import { useTheme } from "@tui/context/theme"
import { Renderable, RGBA } from "@opentui/core" import { Renderable, RGBA } from "@opentui/core"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { createEventBus } from "@solid-primitives/event-bus"
const Border = {
topLeft: "┃",
topRight: "┃",
bottomLeft: "┃",
bottomRight: "┃",
horizontal: "",
vertical: "┃",
topT: "+",
bottomT: "+",
leftT: "+",
rightT: "+",
cross: "+",
}
export function Dialog( export function Dialog(
props: ParentProps<{ props: ParentProps<{
size?: "medium" | "large" size?: "medium" | "large"
@@ -45,11 +31,9 @@ export function Dialog(
onMouseUp={async (e) => { onMouseUp={async (e) => {
e.stopPropagation() e.stopPropagation()
}} }}
customBorderChars={Border}
width={props.size === "large" ? 80 : 60} width={props.size === "large" ? 80 : 60}
maxWidth={dimensions().width - 2} maxWidth={dimensions().width - 2}
backgroundColor={theme.backgroundPanel} backgroundColor={theme.backgroundPanel}
borderColor={theme.border}
paddingTop={1} paddingTop={1}
> >
{props.children} {props.children}
@@ -66,7 +50,6 @@ function init() {
}[], }[],
size: "medium" as "medium" | "large", size: "medium" as "medium" | "large",
}) })
const allClosedEvent = createEventBus<void>()
useKeyboard((evt) => { useKeyboard((evt) => {
if (evt.name === "escape" && store.stack.length > 0) { if (evt.name === "escape" && store.stack.length > 0) {
@@ -97,12 +80,6 @@ function init() {
}, 1) }, 1)
} }
createEffect(() => {
if (store.stack.length === 0) {
allClosedEvent.emit()
}
})
return { return {
clear() { clear() {
for (const item of store.stack) { for (const item of store.stack) {
@@ -115,7 +92,9 @@ function init() {
refocus() refocus()
}, },
replace(input: any, onClose?: () => void) { replace(input: any, onClose?: () => void) {
if (store.stack.length === 0) focus = renderer.currentFocusedRenderable if (store.stack.length === 0) {
focus = renderer.currentFocusedRenderable
}
for (const item of store.stack) { for (const item of store.stack) {
if (item.onClose) item.onClose() if (item.onClose) item.onClose()
} }
@@ -136,9 +115,6 @@ function init() {
setSize(size: "medium" | "large") { setSize(size: "medium" | "large") {
setStore("size", size) setStore("size", size)
}, },
get allClosedEvent() {
return allClosedEvent
}
} }
} }
+13 -1
View File
@@ -49,7 +49,7 @@ function init() {
let timeoutHandle: NodeJS.Timeout | null = null let timeoutHandle: NodeJS.Timeout | null = null
return { const toast = {
show(options: ToastOptions) { show(options: ToastOptions) {
const parsedOptions = TuiEvent.ToastShow.properties.parse(options) const parsedOptions = TuiEvent.ToastShow.properties.parse(options)
const { duration, ...currentToast } = parsedOptions const { duration, ...currentToast } = parsedOptions
@@ -59,10 +59,22 @@ function init() {
setStore("currentToast", null) setStore("currentToast", null)
}, duration).unref() }, duration).unref()
}, },
error: (err: any) => {
if (err instanceof Error)
return toast.show({
variant: "error",
message: err.message,
})
toast.show({
variant: "error",
message: "An unknown error has occurred",
})
},
get currentToast(): ToastOptions | null { get currentToast(): ToastOptions | null {
return store.currentToast return store.currentToast
}, },
} }
return toast
} }
export type ToastContext = ReturnType<typeof init> export type ToastContext = ReturnType<typeof init>
@@ -61,7 +61,7 @@ export namespace Clipboard {
const getCopyMethod = lazy(() => { const getCopyMethod = lazy(() => {
const os = platform() const os = platform()
if (os === "darwin") { if (os === "darwin" && Bun.which("oascript")) {
console.log("clipboard: using osascript") console.log("clipboard: using osascript")
return async (text: string) => { return async (text: string) => {
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"') const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
@@ -70,13 +70,13 @@ export namespace Clipboard {
} }
if (os === "linux") { if (os === "linux") {
if (process.env["WAYLAND_DISPLAY"]) { if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) {
console.log("clipboard: using wl-copy") console.log("clipboard: using wl-copy")
return async (text: string) => { return async (text: string) => {
const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" }) const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
proc.stdin.write(text) proc.stdin.write(text)
proc.stdin.end() proc.stdin.end()
await proc.exited await proc.exited.catch(() => {})
} }
} }
if (Bun.which("xclip")) { if (Bun.which("xclip")) {
@@ -89,7 +89,7 @@ export namespace Clipboard {
}) })
proc.stdin.write(text) proc.stdin.write(text)
proc.stdin.end() proc.stdin.end()
await proc.exited await proc.exited.catch(() => {})
} }
} }
if (Bun.which("xsel")) { if (Bun.which("xsel")) {
@@ -102,7 +102,7 @@ export namespace Clipboard {
}) })
proc.stdin.write(text) proc.stdin.write(text)
proc.stdin.end() proc.stdin.end()
await proc.exited await proc.exited.catch(() => {})
} }
} }
} }
@@ -24,6 +24,7 @@ export namespace Editor {
}) })
await proc.exited await proc.exited
const content = await Bun.file(filepath).text() const content = await Bun.file(filepath).text()
opts.renderer.currentRenderBuffer.clear()
opts.renderer.resume() opts.renderer.resume()
opts.renderer.requestRender() opts.renderer.requestRender()
return content || undefined return content || undefined
+27
View File
@@ -1,8 +1,27 @@
import z from "zod" import z from "zod"
import { Config } from "../config/config" import { Config } from "../config/config"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import PROMPT_INITIALIZE from "./template/initialize.txt"
import { Bus } from "../bus"
import { Identifier } from "../id/id"
export namespace Command { export namespace Command {
export const Default = {
INIT: "init",
} as const
export const Event = {
Executed: Bus.event(
"command.executed",
z.object({
name: z.string(),
sessionID: Identifier.schema("session"),
arguments: z.string(),
messageID: Identifier.schema("message"),
}),
),
}
export const Info = z export const Info = z
.object({ .object({
name: z.string(), name: z.string(),
@@ -33,6 +52,14 @@ export namespace Command {
} }
} }
if (result[Default.INIT] === undefined) {
result[Default.INIT] = {
name: Default.INIT,
description: "create/update AGENTS.md",
template: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
}
}
return result return result
}) })
@@ -6,3 +6,5 @@ The file you create will be given to agentic coding agents (such as yourself) th
If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them. If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
If there's already an AGENTS.md, improve it if it's located in ${path} If there's already an AGENTS.md, improve it if it's located in ${path}
$ARGUMENTS
+26 -3
View File
@@ -385,7 +385,11 @@ export namespace Config {
.optional() .optional()
.default("ctrl+x") .default("ctrl+x")
.describe("Leader key for keybind combinations"), .describe("Leader key for keybind combinations"),
app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"), app_exit: z
.string()
.optional()
.default("ctrl+c,ctrl+d,<leader>q")
.describe("Exit the application"),
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"), editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
theme_list: z.string().optional().default("<leader>t").describe("List available themes"), theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"), sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
@@ -449,18 +453,37 @@ export namespace Config {
.default("<leader>h") .default("<leader>h")
.describe("Toggle code block concealment in messages"), .describe("Toggle code block concealment in messages"),
model_list: z.string().optional().default("<leader>m").describe("List available models"), model_list: z.string().optional().default("<leader>m").describe("List available models"),
model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
model_cycle_recent_reverse: z
.string()
.optional()
.default("shift+f2")
.describe("Previous recently used model"),
command_list: z.string().optional().default("ctrl+p").describe("List available commands"), command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
agent_list: z.string().optional().default("<leader>a").describe("List agents"), agent_list: z.string().optional().default("<leader>a").describe("List agents"),
agent_cycle: z.string().optional().default("tab").describe("Next agent"), agent_cycle: z.string().optional().default("tab").describe("Next agent"),
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"), agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"), input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
input_forward_delete: z.string().optional().default("ctrl+d").describe("Forward delete"),
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"), input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
input_submit: z.string().optional().default("enter").describe("Submit input"), input_submit: z.string().optional().default("return").describe("Submit input"),
input_newline: z input_newline: z
.string() .string()
.optional() .optional()
.default("shift+enter,ctrl+j") .default("shift+return,ctrl+j")
.describe("Insert newline in input"), .describe("Insert newline in input"),
history_previous: z.string().optional().default("up").describe("Previous history item"),
history_next: z.string().optional().default("down").describe("Previous history item"),
session_child_cycle: z
.string()
.optional()
.default("ctrl+right")
.describe("Next child session"),
session_child_cycle_reverse: z
.string()
.optional()
.default("ctrl+left")
.describe("Previous child session"),
}) })
.strict() .strict()
.meta({ .meta({
+1
View File
@@ -367,6 +367,7 @@ export namespace Ripgrep {
args.push(`--max-count=${input.limit}`) args.push(`--max-count=${input.limit}`)
} }
args.push("--")
args.push(input.pattern) args.push(input.pattern)
const command = args.join(" ") const command = args.join(" ")
+26
View File
@@ -2,6 +2,7 @@ import { Bus } from "../bus"
import { File } from "../file" import { File } from "../file"
import { Log } from "../util/log" import { Log } from "../util/log"
import path from "path" import path from "path"
import z from "zod"
import * as Formatter from "./formatter" import * as Formatter from "./formatter"
import { Config } from "../config/config" import { Config } from "../config/config"
@@ -11,6 +12,17 @@ import { Instance } from "../project/instance"
export namespace Format { export namespace Format {
const log = Log.create({ service: "format" }) const log = Log.create({ service: "format" })
export const Status = z
.object({
name: z.string(),
extensions: z.string().array(),
enabled: z.boolean(),
})
.meta({
ref: "FormatterStatus",
})
export type Status = z.infer<typeof Status>
const state = Instance.state(async () => { const state = Instance.state(async () => {
const enabled: Record<string, boolean> = {} const enabled: Record<string, boolean> = {}
const cfg = await Config.get() const cfg = await Config.get()
@@ -62,6 +74,20 @@ export namespace Format {
return result return result
} }
export async function status() {
const s = await state()
const result: Status[] = []
for (const formatter of Object.values(s.formatters)) {
const enabled = await isEnabled(formatter)
result.push({
name: formatter.name,
extensions: formatter.extensions,
enabled,
})
}
return result
}
export function init() { export function init() {
log.info("init") log.info("init")
Bus.subscribe(File.Event.Edited, async (payload) => { Bus.subscribe(File.Event.Edited, async (payload) => {
+2 -1
View File
@@ -38,6 +38,7 @@ process.on("uncaughtException", (e) => {
const cli = yargs(hideBin(process.argv)) const cli = yargs(hideBin(process.argv))
.scriptName("opencode") .scriptName("opencode")
.help("help", "show help") .help("help", "show help")
.alias("help", "h")
.version("version", "show version number", Installation.VERSION) .version("version", "show version number", Installation.VERSION)
.alias("version", "v") .alias("version", "v")
.option("print-logs", { .option("print-logs", {
@@ -140,5 +141,5 @@ try {
// Most notably, some docker-container-based MCP servers don't handle such signals unless // Most notably, some docker-container-based MCP servers don't handle such signals unless
// run using `docker run --init`. // run using `docker run --init`.
// Explicitly exit to avoid any hanging subprocesses. // Explicitly exit to avoid any hanging subprocesses.
process.exit(); process.exit()
} }
@@ -5,6 +5,10 @@ import { LSP } from "../lsp"
import { FileWatcher } from "../file/watcher" import { FileWatcher } from "../file/watcher"
import { File } from "../file" import { File } from "../file"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
import { Project } from "./project"
import { Bus } from "../bus"
import { Command } from "../command"
import { Instance } from "./instance"
export async function InstanceBootstrap() { export async function InstanceBootstrap() {
if (Flag.OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP) return if (Flag.OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP) return
@@ -14,4 +18,10 @@ export async function InstanceBootstrap() {
await LSP.init() await LSP.init()
FileWatcher.init() FileWatcher.init()
File.init() File.init()
Bus.subscribe(Command.Event.Executed, async (payload) => {
if (payload.properties.name === Command.Default.INIT) {
await Project.setInitialized(Instance.project.id)
}
})
} }
+6 -2
View File
@@ -12,7 +12,11 @@ const context = Context.create<Context>("instance")
const cache = new Map<string, Context>() const cache = new Map<string, Context>()
export const Instance = { export const Instance = {
async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> { async provide<R>(input: {
directory: string
init?: () => Promise<any>
fn: () => R
}): Promise<R> {
let existing = cache.get(input.directory) let existing = cache.get(input.directory)
if (!existing) { if (!existing) {
const project = await Project.fromDirectory(input.directory) const project = await Project.fromDirectory(input.directory)
@@ -24,8 +28,8 @@ export const Instance = {
} }
return context.provide(existing, async () => { return context.provide(existing, async () => {
if (!cache.has(input.directory)) { if (!cache.has(input.directory)) {
await input.init?.()
cache.set(input.directory, existing) cache.set(input.directory, existing)
await input.init?.()
} }
return input.fn() return input.fn()
}) })
+21
View File
@@ -20,6 +20,7 @@ import { Ripgrep } from "../file/ripgrep"
import { Config } from "../config/config" import { Config } from "../config/config"
import { File } from "../file" import { File } from "../file"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { Format } from "../format"
import { MessageV2 } from "../session/message-v2" import { MessageV2 } from "../session/message-v2"
import { TuiRoute } from "./tui" import { TuiRoute } from "./tui"
import { Permission } from "../permission" import { Permission } from "../permission"
@@ -1336,6 +1337,26 @@ export namespace Server {
return c.json(await LSP.status()) return c.json(await LSP.status())
}, },
) )
.get(
"/formatter",
describeRoute({
description: "Get formatter status",
operationId: "formatter.status",
responses: {
200: {
description: "Formatter status",
content: {
"application/json": {
schema: resolver(Format.Status.array()),
},
},
},
},
}),
async (c) => {
return c.json(await Format.status())
},
)
.post( .post(
"/tui/append-prompt", "/tui/append-prompt",
describeRoute({ describeRoute({
+14 -18
View File
@@ -2,8 +2,6 @@ import { Decimal } from "decimal.js"
import z from "zod" import z from "zod"
import { type LanguageModelUsage, type ProviderMetadata } from "ai" import { type LanguageModelUsage, type ProviderMetadata } from "ai"
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
import { Bus } from "../bus" import { Bus } from "../bus"
import { Config } from "../config/config" import { Config } from "../config/config"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
@@ -14,11 +12,11 @@ import { Share } from "../share/share"
import { Storage } from "../storage/storage" import { Storage } from "../storage/storage"
import { Log } from "../util/log" import { Log } from "../util/log"
import { MessageV2 } from "./message-v2" import { MessageV2 } from "./message-v2"
import { Project } from "../project/project"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { SessionPrompt } from "./prompt" import { SessionPrompt } from "./prompt"
import { fn } from "@/util/fn" import { fn } from "@/util/fn"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { Command } from "../command"
export namespace Session { export namespace Session {
const log = Log.create({ service: "session" }) const log = Log.create({ service: "session" })
@@ -164,7 +162,12 @@ export namespace Session {
}) })
}) })
export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) { export async function createNext(input: {
id?: string
title?: string
parentID?: string
directory: string
}) {
const result: Info = { const result: Info = {
id: Identifier.descending("session", input.id), id: Identifier.descending("session", input.id),
version: Installation.VERSION, version: Installation.VERSION,
@@ -402,7 +405,9 @@ export namespace Session {
.add(new Decimal(tokens.input).mul(input.model.cost?.input ?? 0).div(1_000_000)) .add(new Decimal(tokens.input).mul(input.model.cost?.input ?? 0).div(1_000_000))
.add(new Decimal(tokens.output).mul(input.model.cost?.output ?? 0).div(1_000_000)) .add(new Decimal(tokens.output).mul(input.model.cost?.output ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.read).mul(input.model.cost?.cache_read ?? 0).div(1_000_000)) .add(new Decimal(tokens.cache.read).mul(input.model.cost?.cache_read ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000)) .add(
new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000),
)
.toNumber(), .toNumber(),
tokens, tokens,
} }
@@ -423,22 +428,13 @@ export namespace Session {
messageID: Identifier.schema("message"), messageID: Identifier.schema("message"),
}), }),
async (input) => { async (input) => {
await SessionPrompt.prompt({ await SessionPrompt.command({
sessionID: input.sessionID, sessionID: input.sessionID,
messageID: input.messageID, messageID: input.messageID,
model: { model: input.providerID + "/" + input.modelID,
providerID: input.providerID, command: Command.Default.INIT,
modelID: input.modelID, arguments: "",
},
parts: [
{
id: Identifier.ascending("part"),
type: "text",
text: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
},
],
}) })
await Project.setInitialized(Instance.project.id)
}, },
) )
} }
+21 -9
View File
@@ -1593,6 +1593,7 @@ export namespace SessionPrompt {
let index = 0 let index = 0
template = template.replace(bashRegex, () => results[index++]) template = template.replace(bashRegex, () => results[index++])
} }
template = template.trim()
const parts = [ const parts = [
{ {
@@ -1657,6 +1658,8 @@ export namespace SessionPrompt {
})() })()
const agent = await Agent.get(agentName) const agent = await Agent.get(agentName)
let result: MessageV2.WithParts
if ((agent.mode === "subagent" && command.subtask !== false) || command.subtask === true) { if ((agent.mode === "subagent" && command.subtask !== false) || command.subtask === true) {
using abort = lock(input.sessionID) using abort = lock(input.sessionID)
@@ -1732,7 +1735,7 @@ export namespace SessionPrompt {
} }
await Session.updatePart(toolPart) await Session.updatePart(toolPart)
const result = await TaskTool.init().then((t) => const taskResult = await TaskTool.init().then((t) =>
t.execute(args, { t.execute(args, {
sessionID: input.sessionID, sessionID: input.sessionID,
abort: abort.signal, abort: abort.signal,
@@ -1760,22 +1763,31 @@ export namespace SessionPrompt {
}, },
input: toolPart.state.input, input: toolPart.state.input,
title: "", title: "",
metadata: result.metadata, metadata: taskResult.metadata,
output: result.output, output: taskResult.output,
} }
await Session.updatePart(toolPart) await Session.updatePart(toolPart)
} }
return { info: assistantMsg, parts: [toolPart] } result = { info: assistantMsg, parts: [toolPart] }
} else {
result = await prompt({
sessionID: input.sessionID,
messageID: input.messageID,
model,
agent: agentName,
parts,
})
} }
return prompt({ Bus.publish(Command.Event.Executed, {
name: input.command,
sessionID: input.sessionID, sessionID: input.sessionID,
messageID: input.messageID, arguments: input.arguments,
model, messageID: result.info.id,
agent: agentName,
parts,
}) })
return result
} }
async function ensureTitle(input: { async function ensureTitle(input: {
+7 -3
View File
@@ -6,10 +6,11 @@ import { generateText, type ModelMessage } from "ai"
import { MessageV2 } from "./message-v2" import { MessageV2 } from "./message-v2"
import { Identifier } from "@/id/id" import { Identifier } from "@/id/id"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { ProviderTransform } from "@/provider/transform" import { ProviderTransform } from "@/provider/transform"
import { SystemPrompt } from "./system" import { SystemPrompt } from "./system"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import path from "path"
import { Instance } from "@/project/instance"
export namespace SessionSummary { export namespace SessionSummary {
const log = Log.create({ service: "session.summary" }) const log = Log.create({ service: "session.summary" })
@@ -33,10 +34,13 @@ export namespace SessionSummary {
input.messages input.messages
.flatMap((x) => x.parts) .flatMap((x) => x.parts)
.filter((x) => x.type === "patch") .filter((x) => x.type === "patch")
.flatMap((x) => x.files), .flatMap((x) => x.files)
.map((x) => path.relative(Instance.worktree, x)),
) )
const diffs = await computeDiff({ messages: input.messages }).then((x) => const diffs = await computeDiff({ messages: input.messages }).then((x) =>
x.filter((x) => files.has(x.file)), x.filter((x) => {
return files.has(x.file)
}),
) )
await Session.update(input.sessionID, (draft) => { await Session.update(input.sessionID, (draft) => {
draft.summary = { draft.summary = {
+1 -1
View File
@@ -20,7 +20,7 @@ export const GrepTool = Tool.define("grep", {
const searchPath = params.path || Instance.directory const searchPath = params.path || Instance.directory
const rgPath = await Ripgrep.filepath() const rgPath = await Ripgrep.filepath()
const args = ["-nH", "--field-match-separator=|", params.pattern] const args = ["-nH", "--field-match-separator=|", "--regexp", params.pattern]
if (params.include) { if (params.include) {
args.push("--glob", params.include) args.push("--glob", params.include)
} }
+2 -2
View File
@@ -26,7 +26,7 @@ Usage notes:
Example usage (NOTE: The agents below are fictional examples for illustration only - use the actual agents listed above): Example usage (NOTE: The agents below are fictional examples for illustration only - use the actual agents listed above):
<example_agent_descriptions> <example_agent_descriptions>
"code-reviewer": use this agent after you are done writing a signficant piece of code "code-reviewer": use this agent after you are done writing a significant piece of code
"greeting-responder": use this agent when to respond to user greetings with a friendly joke "greeting-responder": use this agent when to respond to user greetings with a friendly joke
</example_agent_description> </example_agent_description>
@@ -45,7 +45,7 @@ function isPrime(n) {
} }
</code> </code>
<commentary> <commentary>
Since a signficant piece of code was written and the task was completed, now use the code-reviewer agent to review the code Since a significant piece of code was written and the task was completed, now use the code-reviewer agent to review the code
</commentary> </commentary>
assistant: Now let me use the code-reviewer agent to review the code assistant: Now let me use the code-reviewer agent to review the code
assistant: Uses the Task tool to launch the code-reviewer agent assistant: Uses the Task tool to launch the code-reviewer agent
+18 -6
View File
@@ -28,8 +28,14 @@ describe("Keybind.toString", () => {
}) })
test("should convert shift modifier to string", () => { test("should convert shift modifier to string", () => {
const info: Keybind.Info = { ctrl: false, meta: false, shift: true, leader: false, name: "enter" } const info: Keybind.Info = {
expect(Keybind.toString(info)).toBe("shift+enter") ctrl: false,
meta: false,
shift: true,
leader: false,
name: "return",
}
expect(Keybind.toString(info)).toBe("shift+return")
}) })
test("should convert function key to string", () => { test("should convert function key to string", () => {
@@ -38,7 +44,13 @@ describe("Keybind.toString", () => {
}) })
test("should convert special key to string", () => { test("should convert special key to string", () => {
const info: Keybind.Info = { ctrl: false, meta: false, shift: false, leader: false, name: "pgup" } const info: Keybind.Info = {
ctrl: false,
meta: false,
shift: false,
leader: false,
name: "pgup",
}
expect(Keybind.toString(info)).toBe("pgup") expect(Keybind.toString(info)).toBe("pgup")
}) })
@@ -220,15 +232,15 @@ describe("Keybind.parse", () => {
]) ])
}) })
test("should parse shift+enter combination", () => { test("should parse shift+return combination", () => {
const result = Keybind.parse("shift+enter") const result = Keybind.parse("shift+return")
expect(result).toEqual([ expect(result).toEqual([
{ {
ctrl: false, ctrl: false,
meta: false, meta: false,
shift: true, shift: true,
leader: false, leader: false,
name: "enter", name: "return",
}, },
]) ])
}) })
+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": "1.0.2", "version": "1.0.14",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",
+14 -2
View File
@@ -1,7 +1,18 @@
import { $ } from "bun" import { $ } from "bun"
import path from "path"
if (process.versions.bun !== "1.3.0") { const rootPkgPath = path.resolve(import.meta.dir, "../../../package.json")
throw new Error("This script requires bun@1.3.0") const rootPkg = await Bun.file(rootPkgPath).json()
const expectedBunVersion = rootPkg.packageManager?.split("@")[1]
if (!expectedBunVersion) {
throw new Error("packageManager field not found in root package.json")
}
if (process.versions.bun !== expectedBunVersion) {
throw new Error(
`This script requires bun@${expectedBunVersion}, but you are using bun@${process.versions.bun}`,
)
} }
const CHANNEL = const CHANNEL =
@@ -9,6 +20,7 @@ const CHANNEL =
(await $`git branch --show-current`.text().then((x) => x.trim())) (await $`git branch --show-current`.text().then((x) => x.trim()))
const IS_PREVIEW = CHANNEL !== "latest" const IS_PREVIEW = CHANNEL !== "latest"
const VERSION = await (async () => { const VERSION = await (async () => {
if (process.env["OPENCODE_VERSION"]) return process.env["OPENCODE_VERSION"]
if (IS_PREVIEW) if (IS_PREVIEW)
return `0.0.0-${CHANNEL}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}` return `0.0.0-${CHANNEL}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
const version = await fetch("https://registry.npmjs.org/opencode-ai/latest") const version = await fetch("https://registry.npmjs.org/opencode-ai/latest")
+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": "1.0.2", "version": "1.0.14",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",
+17
View File
@@ -107,6 +107,8 @@ import type {
McpStatusResponses, McpStatusResponses,
LspStatusData, LspStatusData,
LspStatusResponses, LspStatusResponses,
FormatterStatusData,
FormatterStatusResponses,
TuiAppendPromptData, TuiAppendPromptData,
TuiAppendPromptResponses, TuiAppendPromptResponses,
TuiAppendPromptErrors, TuiAppendPromptErrors,
@@ -773,6 +775,20 @@ class Lsp extends _HeyApiClient {
} }
} }
class Formatter extends _HeyApiClient {
/**
* Get formatter status
*/
public status<ThrowOnError extends boolean = false>(
options?: Options<FormatterStatusData, ThrowOnError>,
) {
return (options?.client ?? this._client).get<FormatterStatusResponses, unknown, ThrowOnError>({
url: "/formatter",
...options,
})
}
}
class Control extends _HeyApiClient { class Control extends _HeyApiClient {
/** /**
* Get the next TUI request from the queue * Get the next TUI request from the queue
@@ -1023,6 +1039,7 @@ export class OpencodeClient extends _HeyApiClient {
app = new App({ client: this._client }) app = new App({ client: this._client })
mcp = new Mcp({ client: this._client }) mcp = new Mcp({ client: this._client })
lsp = new Lsp({ client: this._client }) lsp = new Lsp({ client: this._client })
formatter = new Formatter({ client: this._client })
tui = new Tui({ client: this._client }) tui = new Tui({ client: this._client })
auth = new Auth({ client: this._client }) auth = new Auth({ client: this._client })
event = new Event({ client: this._client }) event = new Event({ client: this._client })
+63
View File
@@ -114,6 +114,14 @@ export type KeybindsConfig = {
* List available models * List available models
*/ */
model_list?: string model_list?: string
/**
* Next recently used model
*/
model_cycle_recent?: string
/**
* Previous recently used model
*/
model_cycle_recent_reverse?: string
/** /**
* List available commands * List available commands
*/ */
@@ -134,6 +142,10 @@ export type KeybindsConfig = {
* Clear input field * Clear input field
*/ */
input_clear?: string input_clear?: string
/**
* Forward delete
*/
input_forward_delete?: string
/** /**
* Paste from clipboard * Paste from clipboard
*/ */
@@ -146,6 +158,22 @@ export type KeybindsConfig = {
* Insert newline in input * Insert newline in input
*/ */
input_newline?: string input_newline?: string
/**
* Previous history item
*/
history_previous?: string
/**
* Previous history item
*/
history_next?: string
/**
* Next child session
*/
session_child_cycle?: string
/**
* Previous child session
*/
session_child_cycle_reverse?: string
} }
export type AgentConfig = { export type AgentConfig = {
@@ -1050,6 +1078,12 @@ export type LspStatus = {
status: "connected" | "error" status: "connected" | "error"
} }
export type FormatterStatus = {
name: string
extensions: Array<string>
enabled: boolean
}
export type EventTuiPromptAppend = { export type EventTuiPromptAppend = {
type: "tui.prompt.append" type: "tui.prompt.append"
properties: { properties: {
@@ -1228,6 +1262,16 @@ export type EventTodoUpdated = {
} }
} }
export type EventCommandExecuted = {
type: "command.executed"
properties: {
name: string
sessionID: string
arguments: string
messageID: string
}
}
export type EventSessionIdle = { export type EventSessionIdle = {
type: "session.idle" type: "session.idle"
properties: { properties: {
@@ -1290,6 +1334,7 @@ export type Event =
| EventFileEdited | EventFileEdited
| EventFileWatcherUpdated | EventFileWatcherUpdated
| EventTodoUpdated | EventTodoUpdated
| EventCommandExecuted
| EventSessionIdle | EventSessionIdle
| EventSessionCreated | EventSessionCreated
| EventSessionUpdated | EventSessionUpdated
@@ -2491,6 +2536,24 @@ export type LspStatusResponses = {
export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses] export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses]
export type FormatterStatusData = {
body?: never
path?: never
query?: {
directory?: string
}
url: "/formatter"
}
export type FormatterStatusResponses = {
/**
* Formatter status
*/
200: Array<FormatterStatus>
}
export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses]
export type TuiAppendPromptData = { export type TuiAppendPromptData = {
body?: { body?: {
text: string text: string
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.0.2", "version": "1.0.14",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "bun run src/index.ts", "dev": "bun run src/index.ts",
-4
View File
@@ -1,4 +0,0 @@
opencode-test
cmd/opencode/opencode
opencode
-77
View File
@@ -1,77 +0,0 @@
version: 2
project_name: opencode
before:
hooks:
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
ldflags:
- -s -w -X github.com/sst/opencode/internal/version.Version={{.Version}}
main: ./main.go
archives:
- format: tar.gz
name_template: >-
opencode-
{{- if eq .Os "darwin" }}mac-
{{- else if eq .Os "windows" }}windows-
{{- else if eq .Os "linux" }}linux-{{end}}
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "#86" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
format_overrides:
- goos: windows
format: zip
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "0.0.0-{{ .Timestamp }}"
aurs:
- name: opencode
homepage: "https://github.com/sst/opencode"
description: "terminal based agent that can build anything"
maintainers:
- "dax"
- "adam"
license: "MIT"
private_key: "{{ .Env.AUR_KEY }}"
git_url: "ssh://aur@aur.archlinux.org/opencode-bin.git"
provides:
- opencode
conflicts:
- opencode
package: |-
install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"
brews:
- repository:
owner: sst
name: homebrew-tap
nfpms:
- maintainer: kujtimiihoxha
description: terminal based agent that can build anything
formats:
- deb
- rpm
file_name_template: >-
{{ .ProjectName }}-
{{- if eq .Os "darwin" }}mac
{{- else }}{{ .Os }}{{ end }}-{{ .Arch }}
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^doc:"
- "^test:"
- "^ci:"
- "^ignore:"
- "^example:"
- "^wip:"
-175
View File
@@ -1,175 +0,0 @@
package main
import (
"context"
"io"
"log/slog"
"os"
"os/signal"
"strings"
"syscall"
tea "github.com/charmbracelet/bubbletea/v2"
flag "github.com/spf13/pflag"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode-sdk-go/option"
"github.com/sst/opencode-sdk-go/packages/ssestream"
"github.com/sst/opencode/internal/api"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/clipboard"
"github.com/sst/opencode/internal/decoders"
"github.com/sst/opencode/internal/tui"
"github.com/sst/opencode/internal/util"
"golang.org/x/sync/errgroup"
)
var Version = "dev"
func main() {
version := Version
if version != "dev" && !strings.HasPrefix(Version, "v") {
version = "v" + Version
}
var model *string = flag.String("model", "", "model to begin with")
var prompt *string = flag.String("prompt", "", "prompt to begin with")
var agent *string = flag.String("agent", "", "agent to begin with")
var sessionID *string = flag.String("session", "", "session ID")
flag.Parse()
url := os.Getenv("OPENCODE_SERVER")
stat, err := os.Stdin.Stat()
if err != nil {
slog.Error("Failed to stat stdin", "error", err)
os.Exit(1)
}
// Check if there's data piped to stdin
if (stat.Mode() & os.ModeCharDevice) == 0 {
stdin, err := io.ReadAll(os.Stdin)
if err != nil {
slog.Error("Failed to read stdin", "error", err)
os.Exit(1)
}
stdinContent := strings.TrimSpace(string(stdin))
if stdinContent != "" {
if prompt == nil || *prompt == "" {
prompt = &stdinContent
} else {
combined := *prompt + "\n" + stdinContent
prompt = &combined
}
}
}
// Register custom SSE decoder to handle large events (>32MB)
// This is a workaround for the bufio.Scanner token size limit in the auto-generated SDK
// See: packages/tui/internal/decoders/decoder.go
ssestream.RegisterDecoder("text/event-stream", decoders.NewUnboundedDecoder)
httpClient := opencode.NewClient(
option.WithBaseURL(url),
)
var agents []opencode.Agent
var path *opencode.Path
var project *opencode.Project
batch := errgroup.Group{}
batch.Go(func() error {
result, err := httpClient.Project.Current(context.Background(), opencode.ProjectCurrentParams{})
if err != nil {
return err
}
project = result
return nil
})
batch.Go(func() error {
result, err := httpClient.Agent.List(context.Background(), opencode.AgentListParams{})
if err != nil {
return err
}
agents = *result
return nil
})
batch.Go(func() error {
result, err := httpClient.Path.Get(context.Background(), opencode.PathGetParams{})
if err != nil {
return err
}
path = result
return nil
})
err = batch.Wait()
if err != nil {
panic(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
apiHandler := util.NewAPILogHandler(ctx, httpClient, "tui", slog.LevelDebug)
logger := slog.New(apiHandler)
slog.SetDefault(logger)
slog.Debug("TUI launched")
go func() {
err = clipboard.Init()
if err != nil {
slog.Error("Failed to initialize clipboard", "error", err)
}
}()
// Create main context for the application
app_, err := app.New(ctx, version, project, path, agents, httpClient, model, prompt, agent, sessionID)
if err != nil {
panic(err)
}
tuiModel := tui.NewModel(app_).(*tui.Model)
program := tea.NewProgram(
tuiModel,
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
)
// Set up signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
go func() {
stream := httpClient.Event.ListStreaming(ctx, opencode.EventListParams{})
for stream.Next() {
evt := stream.Current().AsUnion()
program.Send(evt)
}
if err := stream.Err(); err != nil {
slog.Error("Error streaming events", "error", err)
program.Send(err)
}
}()
go api.Start(ctx, program, httpClient)
// Handle signals in a separate goroutine
go func() {
sig := <-sigChan
slog.Info("Received signal, shutting down gracefully", "signal", sig)
tuiModel.Cleanup()
program.Quit()
}()
// Run the TUI
result, err := program.Run()
if err != nil {
slog.Error("TUI error", "error", err)
}
tuiModel.Cleanup()
slog.Info("TUI exited", "result", result)
}
-99
View File
@@ -1,99 +0,0 @@
module github.com/sst/opencode
go 1.24.0
require (
github.com/BurntSushi/toml v1.5.0
github.com/alecthomas/chroma/v2 v2.18.0
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3
github.com/charmbracelet/x/ansi v0.9.3
github.com/fsnotify/fsnotify v1.8.0
github.com/google/uuid v1.6.0
github.com/lithammer/fuzzysearch v1.1.8
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
github.com/muesli/reflow v0.3.0
github.com/muesli/termenv v0.16.0
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
github.com/sst/opencode-sdk-go v0.1.0-alpha.8
golang.org/x/image v0.28.0
rsc.io/qr v0.2.0
)
replace (
github.com/charmbracelet/x/input => ./input
github.com/sst/opencode-sdk-go => ../sdk/go
)
require golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
require (
dario.cat/mergo v1.0.2 // indirect
github.com/atombender/go-jsonschema v0.20.0 // indirect
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/input v0.3.7 // indirect
github.com/charmbracelet/x/windows v0.2.1 // indirect
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
github.com/getkin/kin-openapi v0.127.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/goccy/go-yaml v1.17.1 // indirect
github.com/invopop/yaml v0.3.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sanity-io/litter v1.5.8 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/tools v0.34.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/rivo/uniseg v0.4.7
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/spf13/pflag v1.0.6
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0
gopkg.in/yaml.v3 v3.0.1 // indirect
)
tool (
github.com/atombender/go-jsonschema
github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
)
-313
View File
@@ -1,313 +0,0 @@
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.18.0 h1:6h53Q4hW83SuF+jcsp7CVhLsMozzvQvO8HBbKQW+gn4=
github.com/alecthomas/chroma/v2 v2.18.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atombender/go-jsonschema v0.20.0 h1:AHg0LeI0HcjQ686ALwUNqVJjNRcSXpIR6U+wC2J0aFY=
github.com/atombender/go-jsonschema v0.20.0/go.mod h1:ZmbuR11v2+cMM0PdP6ySxtyZEGFBmhgF4xa4J6Hdls8=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE=
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4 h1:UgUuKKvBwgqm2ZEL+sKv/OLeavrUb4gfHgdxe6oIOno=
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4/go.mod h1:0wWFRpsgF7vHsCukVZ5LAhZkiR4j875H6KEM2/tFQmA=
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3 h1:W6DpZX6zSkZr0iFq6JVh1vItLoxfYtNlaxOJtWp8Kis=
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3/go.mod h1:65HTtKURcv/ict9ZQhr6zT84JqIjMcJbyrZYHHKNfKA=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1 h1:MTSs/nsZNfZPbYk/r9hluK2BtwoqvEYruAujNVwgDv0=
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250505150409-97991a1f17d1/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w=
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY=
github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOgYJZoQbKfEeY/Q=
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1/go.mod h1:N5+lY1tiTDV3V1BeHtOxeWXHoPVeApvsvjJqegfoaz8=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg=
github.com/speakeasy-api/openapi-overlay v0.9.0/go.mod h1:f5FloQrHA7MsxYg9djzMD5h6dxrHjVVByWKh7an8TRc=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
-14
View File
@@ -1,14 +0,0 @@
//go:build !windows
// +build !windows
package input
import (
"io"
"github.com/muesli/cancelreader"
)
func newCancelreader(r io.Reader, _ int) (cancelreader.CancelReader, error) {
return cancelreader.NewReader(r) //nolint:wrapcheck
}

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