Compare commits

...
47 Commits
Author SHA1 Message Date
aron 1bbd84008f move spoof prompt to support anthropic with custom modes (#1031)
publish / publish (push) Has been cancelled
2025-07-15 21:16:27 -04:00
Stephen MurrayandDax Raad fdfd4d69d3 add support for modified gemini-cli system prompt (#1033)
Co-authored-by: Dax Raad <d@ironbay.co>
2025-07-15 21:13:11 -04:00
Jay 7f659cce36 docs: Update README.md 2025-07-15 20:09:26 -04:00
Jay V 48fcaa83be docs: fix config 2025-07-15 19:54:51 -04:00
Jay V 70c16c4c95 docs: adding action to notify discord 2025-07-15 19:49:38 -04:00
Jay V c1e1ef6eb5 docs: readme 2025-07-15 18:32:04 -04:00
Jay V bb155db8b2 docs: share tweak copy button 2025-07-15 18:25:25 -04:00
John Henry RuddenandJay 7c91f668d1 docs: share add copy button to messages in web interface (#902)
Co-authored-by: Jay <air@live.ca>
2025-07-15 17:56:33 -04:00
Jay V 1af103d29e docs: share handle non bundled langs 2025-07-15 17:47:22 -04:00
Jay V 8a3e581edc docs: share fix diff bugs 2025-07-15 17:47:22 -04:00
Jay V 749e7838a4 docs: share page task tool 2025-07-15 17:47:22 -04:00
Dax Raad 73b46c2bf9 docs: document base URL 2025-07-15 14:57:50 -04:00
Joe Schmittandopencode 8bd250fb15 feat(tui): add /export command to export conversation to editor (#989)
Co-authored-by: opencode <noreply@opencode.ai>
2025-07-15 13:53:21 -05:00
Dax Raad b1ab641905 add small model for title generation
publish / publish (push) Has been cancelled
2025-07-15 14:00:52 -04:00
adamdotdevin 76e256ed64 fix(tui): wider max width
publish / publish (push) Has been cancelled
2025-07-15 12:44:41 -05:00
adamdotdevin 4f955f2127 fix(tui): mouse scroll ansi parsing and perf 2025-07-15 12:03:30 -05:00
Aiden Cline bbeb579d3a tweak: (opencode run): adjust tool call rendering, reduce number of "Unknowns" (#1012) 2025-07-15 11:22:57 -05:00
Timo Clasen f707fb3f8d feat(tui): add keymap to remove entries from recently used models (#1019) 2025-07-15 11:20:56 -05:00
adamdotdevin 6b98acb7be chore: update stainless defs 2025-07-15 10:03:11 -05:00
adamdotdevin 2487b18f62 chore: update stainless script to kick off prod build 2025-07-15 08:15:31 -05:00
adamdotdevin 533f64fe26 fix(tui): rework lists and search dialog 2025-07-15 08:07:26 -05:00
Dax Raad b5c85d3806 fix logic for suprpessing snapshots in big directories
publish / publish (push) Has been cancelled
2025-07-15 09:07:04 -04:00
Dax Raad bcf952bc8a upgrade ai sdk 2025-07-15 09:06:35 -04:00
GitHub Action a6dc75a44c ignore: update download stats 2025-07-15 2025-07-15 12:04:28 +00:00
Joohoon Cha 416daca9c6 fix(tui): close completion dialog on ctrl+h (#1005) 2025-07-15 06:24:05 -05:00
Aiden Cline 636fe0fb64 Fix: failed to open session (#999) 2025-07-15 05:40:29 -05:00
Frank 95e0957d64 wip: github actions 2025-07-15 17:45:16 +08:00
Dax Raad 2eefdae6a9 ignore: fix types 2025-07-15 00:56:03 -04:00
Dax Raad d62746ceb7 fix panic
publish / publish (push) Has been cancelled
2025-07-15 00:35:02 -04:00
Dax Raad 4b2ce14ff3 bring back task tool
publish / publish (push) Has been cancelled
2025-07-15 00:05:54 -04:00
Jase KraftandDax Raad 294a11752e fix: --continue pull the latest session id consistently (#918)
Co-authored-by: Dax Raad <d@ironbay.co>
2025-07-14 20:32:00 -04:00
Dax Raad 1cf1d1f634 docs: fix agents.md 2025-07-14 20:23:05 -04:00
Ryan Roden-Corrent 2ce694d41f Add support for job-control suspend (ctrl+z/SIGSTP). (#944) 2025-07-14 20:13:46 -04:00
CodinCat d6eff3b3a3 improve error handling and logging for GitHub API failures in upgrade and install script (#972) 2025-07-14 20:13:12 -04:00
Dax Raad e63a6d45c1 docs: README 2025-07-14 20:10:43 -04:00
Dax Raad 93686519ba docs: README 2025-07-14 20:06:15 -04:00
Mike Wallio f593792fb5 Standardize parameter description references in Edit and MultiEdit tools (#984) 2025-07-14 20:03:59 -04:00
Dax Raad 2cdb37c32b support anthropic console login flow 2025-07-14 18:07:55 -04:00
Timo Clasen 535d79b64c docs: fix typo (#982) 2025-07-14 16:40:16 -04:00
Dax Raad b4e4c3f662 wip: snapshot 2025-07-14 15:29:08 -04:00
adamdotdevin ba676e7ae0 fix(tui): support readline nav in new search component
publish / publish (push) Has been cancelled
2025-07-14 12:20:58 -05:00
adamdotdevin a1c8e5af45 chore: use new search component in find dialog 2025-07-14 12:15:47 -05:00
adamdotdevin f1e7e7c138 feat(tui): even better model selector 2025-07-14 12:15:46 -05:00
Dax Raad 80b77caec0 ignore: share page fix 2025-07-14 13:13:33 -04:00
Dorian Karter 86a2ea44b5 feat(tui): add support for readline list nav (ctrl-p/ctrl-n) (#955) 2025-07-14 10:21:09 -05:00
Dax Raad a2002c88c6 wip: update sdk
publish / publish (push) Has been cancelled
2025-07-14 11:18:08 -04:00
opencode-agent[bot]opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>thdxr
d8bcf4f4e7 Fix issue: Option to update username shown in conversations. (#975)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: thdxr <thdxr@users.noreply.github.com>
2025-07-14 11:03:04 -04:00
83 changed files with 2678 additions and 1790 deletions
+14
View File
@@ -0,0 +1,14 @@
name: Notify Discord on Release
on:
release:
types: [published] # fires only when a release is published
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send nicely-formatted embed to Discord
uses: SethCohen/github-releases-to-discord@v1
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK }}
+15
View File
@@ -0,0 +1,15 @@
# TUI Agent Guidelines
## Style
- prefer single word variable/function names
- avoid try catch where possible - prefer to let exceptions bubble up
- avoid else statements where possible
- do not make useless helper functions - inline functionality unless the
function is reusable or composable
- prefer Bun apis
## Workflow
- you can regenerate the golang sdk by calling ./scripts/stainless.ts
- we use bun for everything
+20 -4
View File
@@ -30,7 +30,8 @@ brew install sst/tap/opencode # macOS
paru -S opencode-bin # Arch Linux paru -S opencode-bin # Arch Linux
``` ```
> **Note:** Remove versions older than 0.1.x before installing > [!TIP]
> Remove versions older than 0.1.x before installing.
### Documentation ### Documentation
@@ -38,10 +39,25 @@ For more info on how to configure opencode [**head over to our docs**](https://o
### Contributing ### Contributing
For any new features we'd appreciate it if you could open an issue first to discuss what you'd like to implement. We're pretty responsive there and it'll save you from working on something that we don't end up using. No need to do this for simpler fixes. opencode is an opinionated tool so any fundamental feature needs to go through a
design process with the core team.
> **Note**: Please talk to us via github issues before spending time working on > [!IMPORTANT]
> a new feature > We do not accept PRs for core features.
However we still merge a ton of PRs - you can contribute:
- Bug fixes
- Improvements to LLM performance
- Support for new providers
- Fixes for env specific quirks
- Missing standard behavior
- Documentation
Take a look at the git history to see what kind of PRs we end up merging.
> [!NOTE]
> If you do not follow the above guidelines we might close your PR.
To run opencode locally you need. To run opencode locally you need.
+1
View File
@@ -16,3 +16,4 @@
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) | | 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) | | 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) | | 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
+11 -158
View File
@@ -30,39 +30,32 @@
}, },
"dependencies": { "dependencies": {
"@clack/prompts": "0.11.0", "@clack/prompts": "0.11.0",
"@flystorage/file-storage": "1.1.0",
"@flystorage/local-fs": "1.1.0",
"@hono/zod-validator": "0.5.0",
"@modelcontextprotocol/sdk": "1.15.1", "@modelcontextprotocol/sdk": "1.15.1",
"@openauthjs/openauth": "0.4.3", "@openauthjs/openauth": "0.4.3",
"@standard-schema/spec": "1.0.0",
"ai": "catalog:", "ai": "catalog:",
"decimal.js": "10.5.0", "decimal.js": "10.5.0",
"diff": "8.0.2", "diff": "8.0.2",
"env-paths": "3.0.0",
"hono": "4.7.10", "hono": "4.7.10",
"hono-openapi": "0.4.8", "hono-openapi": "0.4.8",
"isomorphic-git": "1.32.1", "isomorphic-git": "1.32.1",
"open": "10.1.2", "open": "10.1.2",
"remeda": "2.22.3", "remeda": "2.22.3",
"ts-lsp-client": "1.0.3",
"turndown": "7.2.0", "turndown": "7.2.0",
"vscode-jsonrpc": "8.2.1", "vscode-jsonrpc": "8.2.1",
"vscode-languageclient": "8",
"xdg-basedir": "5.1.0", "xdg-basedir": "5.1.0",
"yargs": "18.0.0", "yargs": "18.0.0",
"zod": "catalog:", "zod": "catalog:",
"zod-openapi": "4.2.4",
"zod-validation-error": "3.5.2",
}, },
"devDependencies": { "devDependencies": {
"@ai-sdk/amazon-bedrock": "2.2.10", "@ai-sdk/amazon-bedrock": "2.2.10",
"@ai-sdk/anthropic": "1.2.12", "@ai-sdk/anthropic": "1.2.12",
"@standard-schema/spec": "1.0.0",
"@tsconfig/bun": "1.0.7", "@tsconfig/bun": "1.0.7",
"@types/bun": "latest", "@types/bun": "latest",
"@types/turndown": "5.0.5", "@types/turndown": "5.0.5",
"@types/yargs": "17.0.33", "@types/yargs": "17.0.33",
"typescript": "catalog:", "typescript": "catalog:",
"vscode-languageserver-types": "3.17.5",
"zod-to-json-schema": "3.24.5", "zod-to-json-schema": "3.24.5",
}, },
}, },
@@ -104,7 +97,7 @@
], ],
"catalog": { "catalog": {
"@types/node": "22.13.9", "@types/node": "22.13.9",
"ai": "5.0.0-beta.15", "ai": "5.0.0-beta.18",
"typescript": "5.8.2", "typescript": "5.8.2",
"zod": "3.25.49", "zod": "3.25.49",
}, },
@@ -113,7 +106,7 @@
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-beta.5", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.2" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-+SgaqoxfFRpFQwgvCK5rh4kznz09x//n9Xtm/l3sjJwlUPLrj+wOeKCCJRWdp1Lpl5cbfdz9qWXrK7Ul+qfUJg=="], "@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-beta.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.2" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-0TOWFetxYximugqdmA/uxk+NRkz51Lyo+anntc3hPzm2OnsbE/Q2wWkKJ14YzoCAfWZ9ZhvxsJRp8xzvvQREsA=="],
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-beta.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Z8SPncMtS3RsoXITmT7NVwrAq6M44dmw0DoUOYJqNNtCu8iMWuxB8Nxsoqpa0uEEy9R1V1ZThJAXTYgjTUxl3w=="], "@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-beta.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Z8SPncMtS3RsoXITmT7NVwrAq6M44dmw0DoUOYJqNNtCu8iMWuxB8Nxsoqpa0uEEy9R1V1ZThJAXTYgjTUxl3w=="],
@@ -275,16 +268,8 @@
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
"@flystorage/dynamic-import": ["@flystorage/dynamic-import@1.0.0", "", {}, "sha512-CIbIUrBdaPFyKnkVBaqzksvzNtsMSXITR/G/6zlil3MBnPFq2LX+X4Mv5p2XOmv/3OulFs/ff2SNb+5dc2Twtg=="],
"@flystorage/file-storage": ["@flystorage/file-storage@1.1.0", "", {}, "sha512-25Gd5EsXDmhHrK5orpRuVqebQms1Cm9m5ACMZ0sVDX+Sbl1V0G88CbcWt7mEoWRYLvQ1U072htqg6Sav76ZlVA=="],
"@flystorage/local-fs": ["@flystorage/local-fs@1.1.0", "", { "dependencies": { "@flystorage/dynamic-import": "^1.0.0", "@flystorage/file-storage": "^1.1.0", "file-type": "^20.5.0", "mime-types": "^3.0.1" } }, "sha512-dbErRhqmCv2UF0zPdeH7iVWuVeTWAJHuJD/mXDe2V370/SL7XIvdE3ditBHWC+1SzBKXJ0lkykOenwlum+oqIA=="],
"@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.5", "", {}, "sha512-G09N3GfuT9qj3Ax2FDZvKqZttzM3v+cco2l8uXamhKyXLdmlaUDH5o88/C3vtTHj2oT7yRKsvxz9F+BXbWKMYA=="], "@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.5", "", {}, "sha512-G09N3GfuT9qj3Ax2FDZvKqZttzM3v+cco2l8uXamhKyXLdmlaUDH5o88/C3vtTHj2oT7yRKsvxz9F+BXbWKMYA=="],
"@hapi/bourne": ["@hapi/bourne@2.1.0", "", {}, "sha512-i1BpaNDVLJdRBEKeJWkVO6tYX6DMFBuwMhSuWqLsY4ufeTKGVuV5rBsUhxPayXqnnWHgXUAmWK16H/ykO5Wj4Q=="],
"@hono/zod-validator": ["@hono/zod-validator@0.5.0", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-ds5bW6DCgAnNHP33E3ieSbaZFd5dkV52ZjyaXtGoR06APFrCtzAsKZxTHwOrJNBdXsi0e5wNwo5L4nVEVnJUdg=="], "@hono/zod-validator": ["@hono/zod-validator@0.5.0", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-ds5bW6DCgAnNHP33E3ieSbaZFd5dkV52ZjyaXtGoR06APFrCtzAsKZxTHwOrJNBdXsi0e5wNwo5L4nVEVnJUdg=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
@@ -483,10 +468,6 @@
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="], "@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
@@ -547,7 +528,7 @@
"acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
"ai": ["ai@5.0.0-beta.15", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-beta.5", "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.2", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-+RpdNV+E551QzWXSFqIjoVkgfMH30sgliTL2yCu4PS2hqWK03CY57Pi0oHcVplw5TLOVeMhs0ax83+dKIJbGIg=="], "ai": ["ai@5.0.0-beta.18", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-beta.7", "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.2", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-gUkO9WF315aT6Al7FxBpY41z3aOVE11BdQUIdIekP1sebys4dElMmKjs9AoaNeYPcf+PklwQ6ZofztiT0egd4A=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
@@ -565,8 +546,6 @@
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"args": ["args@5.0.3", "", { "dependencies": { "camelcase": "5.0.0", "chalk": "2.4.2", "leven": "2.1.0", "mri": "1.1.4" } }, "sha512-h6k/zfFgusnv3i5TU08KQkVKuCPBtL/PWQbWkHUxvJrZ2nAyeaUupneemcrgn1xmqxPQsPIzwkUhOpoqPDRZuA=="],
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="],
@@ -581,8 +560,6 @@
"async-lock": ["async-lock@1.4.1", "", {}, "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="], "async-lock": ["async-lock@1.4.1", "", {}, "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="],
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="], "aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="],
@@ -599,8 +576,6 @@
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"bare-events": ["bare-events@2.5.4", "", {}, "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA=="], "bare-events": ["bare-events@2.5.4", "", {}, "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA=="],
"bare-fs": ["bare-fs@4.1.5", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA=="], "bare-fs": ["bare-fs@4.1.5", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA=="],
@@ -633,8 +608,6 @@
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
"brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="], "brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="],
"browserslist": ["browserslist@4.25.0", "", { "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA=="], "browserslist": ["browserslist@4.25.0", "", { "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA=="],
@@ -731,8 +704,6 @@
"data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="], "data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="],
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="],
@@ -781,8 +752,6 @@
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"electron-to-chromium": ["electron-to-chromium@1.5.161", "", {}, "sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA=="], "electron-to-chromium": ["electron-to-chromium@1.5.161", "", {}, "sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA=="],
@@ -795,8 +764,6 @@
"entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="], "entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="],
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
@@ -815,7 +782,7 @@
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
@@ -863,16 +830,8 @@
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="],
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
"fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="], "fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="],
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="],
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
"flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
@@ -917,8 +876,6 @@
"h3": ["h3@1.15.3", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.4", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.0", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ=="], "h3": ["h3@1.15.3", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.4", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.0", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
@@ -1049,8 +1006,6 @@
"jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="], "jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
"js-base64": ["js-base64@3.7.7", "", {}, "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="], "js-base64": ["js-base64@3.7.7", "", {}, "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
@@ -1059,8 +1014,6 @@
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-rpc-2.0": ["json-rpc-2.0@1.7.0", "", {}, "sha512-asnLgC1qD5ytP+fvBP8uL0rvj+l8P6iYICbzZ8dVxCpESffVjzA7KkYkbKCIbavs7cllwH1ZUaNtJwphdeRqpg=="],
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
@@ -1077,8 +1030,6 @@
"language-map": ["language-map@1.5.0", "", {}, "sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg=="], "language-map": ["language-map@1.5.0", "", {}, "sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg=="],
"leven": ["leven@2.1.0", "", {}, "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
@@ -1225,16 +1176,12 @@
"miniflare": ["miniflare@4.20250525.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250525.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-4PJlT5WA+hfclFU5Q7xnpG1G1VGYTXaf/3iu6iKQ8IsbSi9QvPTA2bSZ5goCFxmJXDjV4cxttVxB0Wl1CLuQ0w=="], "miniflare": ["miniflare@4.20250525.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250525.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-4PJlT5WA+hfclFU5Q7xnpG1G1VGYTXaf/3iu6iKQ8IsbSi9QvPTA2bSZ5goCFxmJXDjV4cxttVxB0Wl1CLuQ0w=="],
"minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"minimisted": ["minimisted@2.0.1", "", { "dependencies": { "minimist": "^1.2.5" } }, "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA=="], "minimisted": ["minimisted@2.0.1", "", { "dependencies": { "minimist": "^1.2.5" } }, "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA=="],
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
"mri": ["mri@1.1.4", "", {}, "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -1279,8 +1226,6 @@
"oidc-token-hash": ["oidc-token-hash@5.1.0", "", {}, "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA=="], "oidc-token-hash": ["oidc-token-hash@5.1.0", "", {}, "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA=="],
"on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
@@ -1327,22 +1272,12 @@
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
"pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="],
"pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="],
"pino-pretty": ["pino-pretty@5.1.3", "", { "dependencies": { "@hapi/bourne": "^2.0.0", "args": "^5.0.1", "chalk": "^4.0.0", "dateformat": "^4.5.1", "fast-safe-stringify": "^2.0.7", "jmespath": "^0.15.0", "joycon": "^3.0.0", "pump": "^3.0.0", "readable-stream": "^3.6.0", "rfdc": "^1.3.0", "split2": "^3.1.1", "strip-json-comments": "^3.1.1" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-Zj+0TVdYKkAAIx9EUCL5e4TttwgsaFvJh2ceIMQeFCY8ak9tseEZQGSgpvyjEj1/iIVGIh5tdhkGEQWSMILKHA=="],
"pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="],
"pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="], "pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
@@ -1361,8 +1296,6 @@
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
"process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="],
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
@@ -1377,8 +1310,6 @@
"querystring": ["querystring@0.2.0", "", {}, "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="], "querystring": ["querystring@0.2.0", "", {}, "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="],
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
@@ -1391,8 +1322,6 @@
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"real-require": ["real-require@0.1.0", "", {}, "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg=="],
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
"recma-jsx": ["recma-jsx@1.0.0", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" } }, "sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q=="], "recma-jsx": ["recma-jsx@1.0.0", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" } }, "sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q=="],
@@ -1449,8 +1378,6 @@
"retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="],
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="], "rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
@@ -1461,8 +1388,6 @@
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="], "sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="],
@@ -1517,16 +1442,12 @@
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
"sonic-boom": ["sonic-boom@2.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg=="],
"source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], "source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="],
"sst": ["sst@3.17.8", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.8", "sst-darwin-x64": "3.17.8", "sst-linux-arm64": "3.17.8", "sst-linux-x64": "3.17.8", "sst-linux-x86": "3.17.8", "sst-win32-arm64": "3.17.8", "sst-win32-x64": "3.17.8", "sst-win32-x86": "3.17.8" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-P/a9/ZsjtQRrTBerBMO1ODaVa5HVTmNLrQNJiYvu2Bgd0ov+vefQeHv6oima8HLlPwpDIPS2gxJk8BZrTZMfCA=="], "sst": ["sst@3.17.8", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.8", "sst-darwin-x64": "3.17.8", "sst-linux-arm64": "3.17.8", "sst-linux-x64": "3.17.8", "sst-linux-x86": "3.17.8", "sst-win32-arm64": "3.17.8", "sst-win32-x64": "3.17.8", "sst-win32-x86": "3.17.8" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-P/a9/ZsjtQRrTBerBMO1ODaVa5HVTmNLrQNJiYvu2Bgd0ov+vefQeHv6oima8HLlPwpDIPS2gxJk8BZrTZMfCA=="],
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-50P6YRMnZVItZUfB0+NzqMww2mmm4vB3zhTVtWUtGoXeiw78g1AEnVlmS28gYXPHM1P987jTvR7EON9u9ig/Dg=="], "sst-darwin-arm64": ["sst-darwin-arm64@3.17.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-50P6YRMnZVItZUfB0+NzqMww2mmm4vB3zhTVtWUtGoXeiw78g1AEnVlmS28gYXPHM1P987jTvR7EON9u9ig/Dg=="],
@@ -1553,8 +1474,6 @@
"stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="],
"stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="],
"streamx": ["streamx@2.22.0", "", { "dependencies": { "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw=="], "streamx": ["streamx@2.22.0", "", { "dependencies": { "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw=="],
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
@@ -1565,24 +1484,18 @@
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="],
"style-to-js": ["style-to-js@1.1.16", "", { "dependencies": { "style-to-object": "1.0.8" } }, "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw=="], "style-to-js": ["style-to-js@1.1.16", "", { "dependencies": { "style-to-object": "1.0.8" } }, "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw=="],
"style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="], "style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tar-fs": ["tar-fs@3.0.9", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA=="], "tar-fs": ["tar-fs@3.0.9", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA=="],
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
"text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="], "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="],
"thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="],
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
@@ -1593,8 +1506,6 @@
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.3", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-3um/NsSq4xFeKbKrNGPHIzfTixwnEVvroqA8Q+lecnYHHJ5TtiYTggHDqewOW+I67t0J1IVBwVKUPjxiQfIcog=="], "toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.3", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-3um/NsSq4xFeKbKrNGPHIzfTixwnEVvroqA8Q+lecnYHHJ5TtiYTggHDqewOW+I67t0J1IVBwVKUPjxiQfIcog=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
@@ -1603,11 +1514,9 @@
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
"ts-lsp-client": ["ts-lsp-client@1.0.3", "", { "dependencies": { "json-rpc-2.0": "^1.7.0", "pino": "^7.0.5", "pino-pretty": "^5.1.3", "tslib": "~2.6.2" } }, "sha512-0ItrsqvNUM9KNFGbeT1N8jSi9gvasGOvxJUXjGf4P2TX0w250AUWLeRStaSrQbYcFDshDtE5d4BshUmYwodDgw=="],
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
"tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
@@ -1621,8 +1530,6 @@
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
"uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
"ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="], "ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="],
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
@@ -1699,11 +1606,7 @@
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="], "vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
"vscode-languageclient": ["vscode-languageclient@8.1.0", "", { "dependencies": { "minimatch": "^5.1.0", "semver": "^7.3.7", "vscode-languageserver-protocol": "3.17.3" } }, "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing=="], "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
"vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.3", "", { "dependencies": { "vscode-jsonrpc": "8.1.0", "vscode-languageserver-types": "3.17.3" } }, "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA=="],
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.3", "", {}, "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA=="],
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
@@ -1763,8 +1666,6 @@
"zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="], "zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
"zod-validation-error": ["zod-validation-error@3.5.2", "", { "peerDependencies": { "zod": "^3.25.0" } }, "sha512-mdi7YOLtram5dzJ5aDtm1AG9+mxRma1iaMrZdYIpFO7epdKBUwLHIxTF8CPDeCQ828zAXYtizrKlEJAtzgfgrw=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
@@ -1783,14 +1684,8 @@
"@astrojs/sitemap/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], "@astrojs/sitemap/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
"@aws-crypto/crc32/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-crypto/util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
@@ -1799,6 +1694,8 @@
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@emnapi/runtime/tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="],
"@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@openauthjs/openauth/@standard-schema/spec": ["@standard-schema/spec@1.0.0-beta.3", "", {}, "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw=="], "@openauthjs/openauth/@standard-schema/spec": ["@standard-schema/spec@1.0.0-beta.3", "", {}, "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw=="],
@@ -1811,28 +1708,10 @@
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@smithy/eventstream-codec/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/is-array-buffer/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/util-buffer-from/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/util-hex-encoding/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/util-utf8/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"args/camelcase": ["camelcase@5.0.0", "", {}, "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA=="],
"args/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
"astro/diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="], "astro/diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="],
"astro/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], "astro/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
@@ -1851,8 +1730,6 @@
"hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="],
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], "miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
"miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], "miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
@@ -1871,18 +1748,10 @@
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"pino-abstract-transport/split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"pino-pretty/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"pino-pretty/jmespath": ["jmespath@0.15.0", "", {}, "sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w=="],
"prebuild-install/tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="], "prebuild-install/tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="],
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"router/path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], "router/path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
"sitemap/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], "sitemap/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="],
@@ -1891,16 +1760,12 @@
"sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], "sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="],
"token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], "unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
"unstorage/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "unstorage/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.1.0", "", {}, "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw=="],
"wrangler/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], "wrangler/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
"yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], "yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
@@ -1917,18 +1782,12 @@
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"args/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
"args/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
"bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
"opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
"pino-pretty/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], "prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
@@ -1984,11 +1843,5 @@
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"args/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
"args/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
"args/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
} }
} }
+1 -1
View File
@@ -48,7 +48,7 @@ if [ -z "$requested_version" ]; then
url="https://github.com/sst/opencode/releases/latest/download/$filename" url="https://github.com/sst/opencode/releases/latest/download/$filename"
specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | awk -F'"' '/"tag_name": "/ {gsub(/^v/, "", $4); print $4}') specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | awk -F'"' '/"tag_name": "/ {gsub(/^v/, "", $4); print $4}')
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 || -z "$specific_version" ]]; then
echo "${RED}Failed to fetch version information${NC}" echo "${RED}Failed to fetch version information${NC}"
exit 1 exit 1
fi fi
+2 -4
View File
@@ -18,7 +18,7 @@
"typescript": "5.8.2", "typescript": "5.8.2",
"@types/node": "22.13.9", "@types/node": "22.13.9",
"zod": "3.25.49", "zod": "3.25.49",
"ai": "5.0.0-beta.15" "ai": "5.0.0-beta.18"
} }
}, },
"devDependencies": { "devDependencies": {
@@ -39,7 +39,5 @@
"protobufjs", "protobufjs",
"sharp" "sharp"
], ],
"patchedDependencies": { "patchedDependencies": {}
"ai@4.3.16": "patches/ai@4.3.16.patch"
}
} }
+35
View File
@@ -232,6 +232,9 @@ export default {
) )
} }
/**
* Used by the GitHub action to get GitHub installation access token given the OIDC token
*/
if (request.method === "POST" && method === "exchange_github_app_token") { if (request.method === "POST" && method === "exchange_github_app_token") {
const EXPECTED_AUDIENCE = "opencode-github-action" const EXPECTED_AUDIENCE = "opencode-github-action"
const GITHUB_ISSUER = "https://token.actions.githubusercontent.com" const GITHUB_ISSUER = "https://token.actions.githubusercontent.com"
@@ -285,6 +288,38 @@ export default {
}) })
} }
/**
* Used by the opencode CLI to check if the GitHub app is installed
*/
if (request.method === "GET" && method === "get_github_app_installation") {
const owner = url.searchParams.get("owner")
const repo = url.searchParams.get("repo")
const auth = createAppAuth({
appId: Resource.GITHUB_APP_ID.value,
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
})
const appAuth = await auth({ type: "app" })
// Lookup installation
const octokit = new Octokit({ auth: appAuth.token })
let installation
try {
const ret = await octokit.apps.getRepoInstallation({ owner, repo })
installation = ret.data
} catch (err) {
if (err instanceof Error && err.message.includes("Not Found")) {
// not installed
} else {
throw err
}
}
return new Response(JSON.stringify({ installation }), {
headers: { "Content-Type": "application/json" },
})
}
return new Response("Not Found", { status: 404 }) return new Response("Not Found", { status: 404 })
}, },
} }
+3 -10
View File
@@ -17,38 +17,31 @@
"devDependencies": { "devDependencies": {
"@ai-sdk/amazon-bedrock": "2.2.10", "@ai-sdk/amazon-bedrock": "2.2.10",
"@ai-sdk/anthropic": "1.2.12", "@ai-sdk/anthropic": "1.2.12",
"@standard-schema/spec": "1.0.0",
"@tsconfig/bun": "1.0.7", "@tsconfig/bun": "1.0.7",
"@types/bun": "latest", "@types/bun": "latest",
"@types/turndown": "5.0.5", "@types/turndown": "5.0.5",
"@types/yargs": "17.0.33", "@types/yargs": "17.0.33",
"typescript": "catalog:", "typescript": "catalog:",
"vscode-languageserver-types": "3.17.5",
"zod-to-json-schema": "3.24.5" "zod-to-json-schema": "3.24.5"
}, },
"dependencies": { "dependencies": {
"@clack/prompts": "0.11.0", "@clack/prompts": "0.11.0",
"@flystorage/file-storage": "1.1.0",
"@flystorage/local-fs": "1.1.0",
"@hono/zod-validator": "0.5.0",
"@modelcontextprotocol/sdk": "1.15.1", "@modelcontextprotocol/sdk": "1.15.1",
"@openauthjs/openauth": "0.4.3", "@openauthjs/openauth": "0.4.3",
"@standard-schema/spec": "1.0.0",
"ai": "catalog:", "ai": "catalog:",
"decimal.js": "10.5.0", "decimal.js": "10.5.0",
"diff": "8.0.2", "diff": "8.0.2",
"env-paths": "3.0.0",
"hono": "4.7.10", "hono": "4.7.10",
"hono-openapi": "0.4.8", "hono-openapi": "0.4.8",
"isomorphic-git": "1.32.1", "isomorphic-git": "1.32.1",
"open": "10.1.2", "open": "10.1.2",
"remeda": "2.22.3", "remeda": "2.22.3",
"ts-lsp-client": "1.0.3",
"turndown": "7.2.0", "turndown": "7.2.0",
"vscode-jsonrpc": "8.2.1", "vscode-jsonrpc": "8.2.1",
"vscode-languageclient": "8",
"xdg-basedir": "5.1.0", "xdg-basedir": "5.1.0",
"yargs": "18.0.0", "yargs": "18.0.0",
"zod": "catalog:", "zod": "catalog:"
"zod-openapi": "4.2.4",
"zod-validation-error": "3.5.2"
} }
} }
-2
View File
@@ -12,7 +12,6 @@ export namespace App {
export const Info = z export const Info = z
.object({ .object({
user: z.string(),
hostname: z.string(), hostname: z.string(),
git: z.boolean(), git: z.boolean(),
path: z.object({ path: z.object({
@@ -70,7 +69,6 @@ export namespace App {
const root = git ?? input.cwd const root = git ?? input.cwd
const info: Info = { const info: Info = {
user: os.userInfo().username,
hostname: os.hostname(), hostname: os.hostname(),
time: { time: {
initialized: state.initialized, initialized: state.initialized,
+8 -5
View File
@@ -4,9 +4,13 @@ import { Auth } from "./index"
export namespace AuthAnthropic { export namespace AuthAnthropic {
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
export async function authorize() { export async function authorize(mode: "max" | "console") {
const pkce = await generatePKCE() const pkce = await generatePKCE()
const url = new URL("https://claude.ai/oauth/authorize", import.meta.url)
const url = new URL(
`https://${mode === "console" ? "console.anthropic.com" : "claude.ai"}/oauth/authorize`,
import.meta.url,
)
url.searchParams.set("code", "true") url.searchParams.set("code", "true")
url.searchParams.set("client_id", CLIENT_ID) url.searchParams.set("client_id", CLIENT_ID)
url.searchParams.set("response_type", "code") url.searchParams.set("response_type", "code")
@@ -39,12 +43,11 @@ export namespace AuthAnthropic {
}) })
if (!result.ok) throw new ExchangeFailed() if (!result.ok) throw new ExchangeFailed()
const json = await result.json() const json = await result.json()
await Auth.set("anthropic", { return {
type: "oauth",
refresh: json.refresh_token as string, refresh: json.refresh_token as string,
access: json.access_token as string, access: json.access_token as string,
expires: Date.now() + json.expires_in * 1000, expires: Date.now() + json.expires_in * 1000,
}) }
} }
export async function access() { export async function access() {
+66 -9
View File
@@ -132,20 +132,24 @@ export const AuthLoginCommand = cmd({
options: [ options: [
{ {
label: "Claude Pro/Max", label: "Claude Pro/Max",
value: "oauth", value: "max",
}, },
{ {
label: "API Key", label: "Create API Key",
value: "console",
},
{
label: "Manually enter API Key",
value: "api", value: "api",
}, },
], ],
}) })
if (prompts.isCancel(method)) throw new UI.CancelledError() if (prompts.isCancel(method)) throw new UI.CancelledError()
if (method === "oauth") { if (method === "max") {
// some weird bug where program exits without this // some weird bug where program exits without this
await new Promise((resolve) => setTimeout(resolve, 10)) await new Promise((resolve) => setTimeout(resolve, 10))
const { url, verifier } = await AuthAnthropic.authorize() const { url, verifier } = await AuthAnthropic.authorize("max")
prompts.note("Trying to open browser...") prompts.note("Trying to open browser...")
try { try {
await open(url) await open(url)
@@ -162,13 +166,66 @@ export const AuthLoginCommand = cmd({
}) })
if (prompts.isCancel(code)) throw new UI.CancelledError() if (prompts.isCancel(code)) throw new UI.CancelledError()
await AuthAnthropic.exchange(code, verifier) try {
.then(() => { const credentials = await AuthAnthropic.exchange(code, verifier)
prompts.log.success("Login successful") await Auth.set("anthropic", {
type: "oauth",
refresh: credentials.refresh,
access: credentials.access,
expires: credentials.expires,
}) })
.catch(() => { prompts.log.success("Login successful")
prompts.log.error("Invalid code") } catch {
prompts.log.error("Invalid code")
}
prompts.outro("Done")
return
}
if (method === "console") {
// some weird bug where program exits without this
await new Promise((resolve) => setTimeout(resolve, 10))
const { url, verifier } = await AuthAnthropic.authorize("console")
prompts.note("Trying to open browser...")
try {
await open(url)
} catch (e) {
prompts.log.error(
"Failed to open browser perhaps you are running without a display or X server, please open the following URL in your browser:",
)
}
prompts.log.info(url)
const code = await prompts.text({
message: "Paste the authorization code here: ",
validate: (x) => (x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(code)) throw new UI.CancelledError()
try {
const credentials = await AuthAnthropic.exchange(code, verifier)
const accessToken = credentials.access
const response = await fetch("https://api.anthropic.com/api/oauth/claude_cli/create_api_key", {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json, text/plain, */*",
},
}) })
if (!response.ok) {
throw new Error("Failed to create API key")
}
const json = await response.json()
await Auth.set("anthropic", {
type: "api",
key: json.raw_key,
})
prompts.log.success("Login successful - API key created and saved")
} catch (error) {
prompts.log.error("Invalid code or failed to create API key")
}
prompts.outro("Done") prompts.outro("Done")
return return
} }
@@ -4,11 +4,11 @@ import { cmd } from "../cmd"
export const SnapshotCommand = cmd({ export const SnapshotCommand = cmd({
command: "snapshot", command: "snapshot",
builder: (yargs) => yargs.command(SnapshotCreateCommand).command(SnapshotRestoreCommand).demandCommand(), builder: (yargs) => yargs.command(CreateCommand).command(RestoreCommand).command(DiffCommand).demandCommand(),
async handler() {}, async handler() {},
}) })
export const SnapshotCreateCommand = cmd({ const CreateCommand = cmd({
command: "create", command: "create",
async handler() { async handler() {
await bootstrap({ cwd: process.cwd() }, async () => { await bootstrap({ cwd: process.cwd() }, async () => {
@@ -18,7 +18,7 @@ export const SnapshotCreateCommand = cmd({
}, },
}) })
export const SnapshotRestoreCommand = cmd({ const RestoreCommand = cmd({
command: "restore <commit>", command: "restore <commit>",
builder: (yargs) => builder: (yargs) =>
yargs.positional("commit", { yargs.positional("commit", {
@@ -33,3 +33,20 @@ export const SnapshotRestoreCommand = cmd({
}) })
}, },
}) })
export const DiffCommand = cmd({
command: "diff <commit>",
describe: "diff",
builder: (yargs) =>
yargs.positional("commit", {
type: "string",
description: "commit",
demandOption: true,
}),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
const diff = await Snapshot.diff("test", args.commit)
console.log(diff)
})
},
})
+3 -1
View File
@@ -124,7 +124,9 @@ export const RunCommand = cmd({
if (part.type === "tool" && part.state.status === "completed") { if (part.type === "tool" && part.state.status === "completed") {
const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD] const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD]
printEvent(color, tool, part.state.title || "Unknown") const title =
part.state.title || Object.keys(part.state.input).length > 0 ? JSON.stringify(part.state.input) : "Unknown"
printEvent(color, tool, title)
} }
if (part.type === "text") { if (part.type === "text") {
+24 -9
View File
@@ -21,6 +21,17 @@ export namespace Config {
result = mergeDeep(result, await load(resolved)) result = mergeDeep(result, await load(resolved))
} }
} }
// Handle migration from autoshare to share field
if (result.autoshare === true && !result.share) {
result.share = "auto"
}
if (!result.username) {
const os = await import("os")
result.username = os.userInfo().username
}
log.info("loaded", result) log.info("loaded", result)
return result return result
@@ -72,6 +83,7 @@ export namespace Config {
app_help: z.string().optional().default("<leader>h").describe("Show help dialog"), app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
switch_mode: z.string().optional().default("tab").describe("Switch mode"), switch_mode: z.string().optional().default("tab").describe("Switch mode"),
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"), editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
session_new: z.string().optional().default("<leader>n").describe("Create a new session"), session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
session_list: z.string().optional().default("<leader>l").describe("List all sessions"), session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
session_share: z.string().optional().default("<leader>s").describe("Share current session"), session_share: z.string().optional().default("<leader>s").describe("Share current session"),
@@ -117,11 +129,21 @@ export namespace Config {
$schema: z.string().optional().describe("JSON schema reference for configuration validation"), $schema: z.string().optional().describe("JSON schema reference for configuration validation"),
theme: z.string().optional().describe("Theme name to use for the interface"), theme: z.string().optional().describe("Theme name to use for the interface"),
keybinds: Keybinds.optional().describe("Custom keybind configurations"), keybinds: Keybinds.optional().describe("Custom keybind configurations"),
share: z.enum(["auto", "disabled"]).optional().describe("Control sharing behavior: 'auto' enables automatic sharing, 'disabled' disables all sharing"), share: z
autoshare: z.boolean().optional().describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"), .enum(["auto", "disabled"])
.optional()
.describe("Control sharing behavior: 'auto' enables automatic sharing, 'disabled' disables all sharing"),
autoshare: z
.boolean()
.optional()
.describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"), autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"), disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
username: z
.string()
.optional()
.describe("Custom username to display in conversations instead of system username"),
mode: z mode: z
.object({ .object({
build: Mode.optional(), build: Mode.optional(),
@@ -233,11 +255,6 @@ export namespace Config {
const parsed = Info.safeParse(data) const parsed = Info.safeParse(data)
if (parsed.success) { if (parsed.success) {
// Handle migration from autoshare to share field
if (parsed.data.autoshare === true && !parsed.data.share) {
parsed.data.share = "auto"
}
if (!parsed.data.$schema) { if (!parsed.data.$schema) {
parsed.data.$schema = "https://opencode.ai/config.json" parsed.data.$schema = "https://opencode.ai/config.json"
await Bun.write(configPath, JSON.stringify(parsed.data, null, 2)) await Bun.write(configPath, JSON.stringify(parsed.data, null, 2))
@@ -264,6 +281,4 @@ export namespace Config {
export function get() { export function get() {
return state() return state()
} }
} }
+7 -1
View File
@@ -140,6 +140,12 @@ export namespace Installation {
export async function latest() { export async function latest() {
return fetch("https://api.github.com/repos/sst/opencode/releases/latest") return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
.then((res) => res.json()) .then((res) => res.json())
.then((data) => data.tag_name.slice(1) as string) .then((data) => {
if (typeof data.tag_name !== "string") {
log.error("GitHub API error", data)
throw new Error("failed to fetch latest version")
}
return data.tag_name.slice(1) as string
})
} }
} }
+13 -10
View File
@@ -21,7 +21,7 @@ import { AuthCopilot } from "../auth/copilot"
import { ModelsDev } from "./models" import { ModelsDev } from "./models"
import { NamedError } from "../util/error" import { NamedError } from "../util/error"
import { Auth } from "../auth" import { Auth } from "../auth"
// import { TaskTool } from "../tool/task" import { TaskTool } from "../tool/task"
export namespace Provider { export namespace Provider {
const log = Log.create({ service: "provider" }) const log = Log.create({ service: "provider" })
@@ -408,6 +408,17 @@ export namespace Provider {
} }
} }
export async function getSmallModel(providerID: string) {
const provider = await state().then((state) => state.providers[providerID])
if (!provider) return
const priority = ["3-5-haiku", "3.5-haiku", "gemini-2.5-flash"]
for (const item of priority) {
for (const model of Object.keys(provider.info.models)) {
if (model.includes(item)) return getModel(providerID, model)
}
}
}
const priority = ["gemini-2.5-pro-preview", "codex-mini", "claude-sonnet-4"] const priority = ["gemini-2.5-pro-preview", "codex-mini", "claude-sonnet-4"]
export function sort(models: ModelsDev.Model[]) { export function sort(models: ModelsDev.Model[]) {
return sortBy( return sortBy(
@@ -456,7 +467,7 @@ export namespace Provider {
WriteTool, WriteTool,
TodoWriteTool, TodoWriteTool,
TodoReadTool, TodoReadTool,
// TaskTool, TaskTool,
] ]
const TOOL_MAPPING: Record<string, Tool.Info[]> = { const TOOL_MAPPING: Record<string, Tool.Info[]> = {
@@ -531,12 +542,4 @@ export namespace Provider {
providerID: z.string(), providerID: z.string(),
}), }),
) )
export const AuthError = NamedError.create(
"ProviderAuthError",
z.object({
providerID: z.string(),
message: z.string(),
}),
)
} }
+257 -222
View File
@@ -17,6 +17,7 @@ import {
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt" import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
import PROMPT_PLAN from "../session/prompt/plan.txt" import PROMPT_PLAN from "../session/prompt/plan.txt"
import PROMPT_ANTHROPIC_SPOOF from "../session/prompt/anthropic_spoof.txt"
import { App } from "../app/app" import { App } from "../app/app"
import { Bus } from "../bus" import { Bus } from "../bus"
@@ -230,8 +231,7 @@ export namespace Session {
info: MessageV2.Info info: MessageV2.Info
parts: MessageV2.Part[] parts: MessageV2.Part[]
}[] }[]
const list = Storage.list("session/message/" + sessionID) for (const p of await Storage.list("session/message/" + sessionID)) {
for await (const p of list) {
const read = await Storage.readJSON<MessageV2.Info>(p) const read = await Storage.readJSON<MessageV2.Info>(p)
result.push({ result.push({
info: read, info: read,
@@ -248,7 +248,7 @@ export namespace Session {
export async function parts(sessionID: string, messageID: string) { export async function parts(sessionID: string, messageID: string) {
const result = [] as MessageV2.Part[] const result = [] as MessageV2.Part[]
for await (const item of Storage.list("session/part/" + sessionID + "/" + messageID)) { for (const item of await Storage.list("session/part/" + sessionID + "/" + messageID)) {
const read = await Storage.readJSON<MessageV2.Part>(item) const read = await Storage.readJSON<MessageV2.Part>(item)
result.push(read) result.push(read)
} }
@@ -257,7 +257,7 @@ export namespace Session {
} }
export async function* list() { export async function* list() {
for await (const item of Storage.list("session/info")) { for (const item of await Storage.list("session/info")) {
const sessionID = path.basename(item, ".json") const sessionID = path.basename(item, ".json")
yield get(sessionID) yield get(sessionID)
} }
@@ -265,7 +265,7 @@ export namespace Session {
export async function children(parentID: string) { export async function children(parentID: string) {
const result = [] as Session.Info[] const result = [] as Session.Info[]
for await (const item of Storage.list("session/info")) { for (const item of await Storage.list("session/info")) {
const sessionID = path.basename(item, ".json") const sessionID = path.basename(item, ".json")
const session = await get(sessionID) const session = await get(sessionID)
if (session.parentID !== parentID) continue if (session.parentID !== parentID) continue
@@ -444,7 +444,7 @@ export namespace Session {
const result = await ReadTool.execute(args, { const result = await ReadTool.execute(args, {
sessionID: input.sessionID, sessionID: input.sessionID,
abort: abort.signal, abort: abort.signal,
messageID: "", // read tool doesn't use message ID messageID: userMsg.id,
metadata: async () => {}, metadata: async () => {},
}) })
return [ return [
@@ -505,6 +505,7 @@ export namespace Session {
}) })
if (msgs.length === 0 && !session.parentID) { if (msgs.length === 0 && !session.parentID) {
const small = (await Provider.getSmallModel(input.providerID)) ?? model
generateText({ generateText({
maxOutputTokens: input.providerID === "google" ? 1024 : 20, maxOutputTokens: input.providerID === "google" ? 1024 : 20,
providerOptions: model.info.options, providerOptions: model.info.options,
@@ -529,7 +530,7 @@ export namespace Session {
}, },
]), ]),
], ],
model: model.language, model: small.language,
}) })
.then((result) => { .then((result) => {
if (result.text) if (result.text)
@@ -546,7 +547,8 @@ export namespace Session {
msgs.push({ info: userMsg, parts: userParts }) msgs.push({ info: userMsg, parts: userParts })
const mode = await Mode.get(input.mode ?? "build") const mode = await Mode.get(input.mode ?? "build")
let system = mode.prompt ? [mode.prompt] : SystemPrompt.provider(input.providerID, input.modelID) let system = input.providerID === "anthropic" ? [PROMPT_ANTHROPIC_SPOOF.trim()] : []
system.push(...(mode.prompt ? [mode.prompt] : SystemPrompt.provider(input.modelID)))
system.push(...(await SystemPrompt.environment())) system.push(...(await SystemPrompt.environment()))
system.push(...(await SystemPrompt.custom())) system.push(...(await SystemPrompt.custom()))
// max 2 system prompt messages for caching purposes // max 2 system prompt messages for caching purposes
@@ -578,20 +580,22 @@ export namespace Session {
await updateMessage(assistantMsg) await updateMessage(assistantMsg)
const tools: Record<string, AITool> = {} const tools: Record<string, AITool> = {}
const processor = createProcessor(assistantMsg, model.info)
for (const item of await Provider.tools(input.providerID)) { for (const item of await Provider.tools(input.providerID)) {
if (mode.tools[item.id] === false) continue if (mode.tools[item.id] === false) continue
if (session.parentID && item.id === "task") continue
tools[item.id] = tool({ tools[item.id] = tool({
id: item.id as any, id: item.id as any,
description: item.description, description: item.description,
inputSchema: item.parameters as ZodSchema, inputSchema: item.parameters as ZodSchema,
async execute(args) { async execute(args, options) {
const result = await item.execute(args, { const result = await item.execute(args, {
sessionID: input.sessionID, sessionID: input.sessionID,
abort: abort.signal, abort: abort.signal,
messageID: assistantMsg.id, messageID: assistantMsg.id,
metadata: async () => { metadata: async (val) => {
/* const match = processor.partFromToolCall(options.toolCallId)
const match = toolCalls[opts.toolCallId]
if (match && match.state.status === "running") { if (match && match.state.status === "running") {
await updatePart({ await updatePart({
...match, ...match,
@@ -599,14 +603,13 @@ export namespace Session {
title: val.title, title: val.title,
metadata: val.metadata, metadata: val.metadata,
status: "running", status: "running",
input: args.input, input: args,
time: { time: {
start: Date.now(), start: Date.now(),
}, },
}, },
}) })
} }
*/
}, },
}) })
return result return result
@@ -677,230 +680,260 @@ export namespace Session {
], ],
}), }),
}) })
const result = await processStream(assistantMsg, model.info, stream) const result = await processor.process(stream)
return result return result
} }
async function processStream( function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) {
assistantMsg: MessageV2.Assistant, const toolCalls: Record<string, MessageV2.ToolPart> = {}
model: ModelsDev.Model, return {
stream: StreamTextResult<Record<string, AITool>, never>, partFromToolCall(toolCallID: string) {
) { return toolCalls[toolCallID]
try { },
let currentText: MessageV2.TextPart | undefined async process(stream: StreamTextResult<Record<string, AITool>, never>) {
const toolCalls: Record<string, MessageV2.ToolPart> = {} try {
let currentText: MessageV2.TextPart | undefined
for await (const value of stream.fullStream) { for await (const value of stream.fullStream) {
log.info("part", { log.info("part", {
type: value.type, type: value.type,
})
switch (value.type) {
case "start":
break
case "tool-input-start":
const part = await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "tool",
tool: value.toolName,
callID: value.id,
state: {
status: "pending",
},
}) })
toolCalls[value.id] = part as MessageV2.ToolPart switch (value.type) {
break case "start":
const snapshot = await Snapshot.create(assistantMsg.sessionID)
if (snapshot)
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "snapshot",
snapshot,
})
break
case "tool-input-delta": case "tool-input-start":
break const part = await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "tool",
tool: value.toolName,
callID: value.id,
state: {
status: "pending",
},
})
toolCalls[value.id] = part as MessageV2.ToolPart
break
case "tool-call": { case "tool-input-delta":
const match = toolCalls[value.toolCallId] break
if (match) {
const part = await updatePart({ case "tool-call": {
...match, const match = toolCalls[value.toolCallId]
state: { if (match) {
status: "running", const part = await updatePart({
input: value.input, ...match,
state: {
status: "running",
input: value.input,
time: {
start: Date.now(),
},
},
})
toolCalls[value.toolCallId] = part as MessageV2.ToolPart
}
break
}
case "tool-result": {
const match = toolCalls[value.toolCallId]
if (match && match.state.status === "running") {
await updatePart({
...match,
state: {
status: "completed",
input: value.input,
output: value.output.output,
metadata: value.output.metadata,
title: value.output.title,
time: {
start: match.state.time.start,
end: Date.now(),
},
},
})
delete toolCalls[value.toolCallId]
const snapshot = await Snapshot.create(assistantMsg.sessionID)
if (snapshot)
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "snapshot",
snapshot,
})
}
break
}
case "tool-error": {
const match = toolCalls[value.toolCallId]
if (match && match.state.status === "running") {
await updatePart({
...match,
state: {
status: "error",
input: value.input,
error: (value.error as any).toString(),
time: {
start: match.state.time.start,
end: Date.now(),
},
},
})
delete toolCalls[value.toolCallId]
const snapshot = await Snapshot.create(assistantMsg.sessionID)
if (snapshot)
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "snapshot",
snapshot,
})
}
break
}
case "error":
throw value.error
case "start-step":
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "step-start",
})
break
case "finish-step":
const usage = getUsage(model, value.usage, value.providerMetadata)
assistantMsg.cost += usage.cost
assistantMsg.tokens = usage.tokens
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "step-finish",
tokens: usage.tokens,
cost: usage.cost,
})
await updateMessage(assistantMsg)
break
case "text-start":
currentText = {
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "text",
text: "",
time: { time: {
start: Date.now(), start: Date.now(),
}, },
}, }
}) break
toolCalls[value.toolCallId] = part as MessageV2.ToolPart
} case "text":
break if (currentText) {
} currentText.text += value.text
case "tool-result": { await updatePart(currentText)
const match = toolCalls[value.toolCallId] }
if (match && match.state.status === "running") { break
await updatePart({
...match, case "text-end":
state: { if (currentText && currentText.text) {
status: "completed", currentText.time = {
input: value.input, start: Date.now(),
output: value.output.output,
metadata: value.output.metadata,
title: value.output.title,
time: {
start: match.state.time.start,
end: Date.now(), end: Date.now(),
}, }
}, await updatePart(currentText)
}) }
delete toolCalls[value.toolCallId] currentText = undefined
break
case "finish":
assistantMsg.time.completed = Date.now()
await updateMessage(assistantMsg)
break
default:
log.info("unhandled", {
...value,
})
continue
} }
break
} }
} catch (e) {
case "tool-error": { log.error("", {
const match = toolCalls[value.toolCallId] error: e,
if (match && match.state.status === "running") { })
await updatePart({ switch (true) {
...match, case e instanceof DOMException && e.name === "AbortError":
state: { assistantMsg.error = new MessageV2.AbortedError(
status: "error", { message: e.message },
input: value.input, {
error: (value.error as any).toString(), cause: e,
time: {
start: match.state.time.start,
end: Date.now(),
},
}, },
}) ).toObject()
delete toolCalls[value.toolCallId] break
} case MessageV2.OutputLengthError.isInstance(e):
break assistantMsg.error = e
break
case LoadAPIKeyError.isInstance(e):
assistantMsg.error = new MessageV2.AuthError(
{
providerID: model.id,
message: e.message,
},
{ cause: e },
).toObject()
break
case e instanceof Error:
assistantMsg.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
break
default:
assistantMsg.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
} }
Bus.publish(Event.Error, {
case "error": sessionID: assistantMsg.sessionID,
throw value.error error: assistantMsg.error,
})
case "start-step":
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "step-start",
})
break
case "finish-step":
const usage = getUsage(model, value.usage, value.providerMetadata)
assistantMsg.cost += usage.cost
assistantMsg.tokens = usage.tokens
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "step-finish",
tokens: usage.tokens,
cost: usage.cost,
})
await updateMessage(assistantMsg)
break
case "text-start":
currentText = {
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "text",
text: "",
time: {
start: Date.now(),
},
}
break
case "text":
if (currentText) {
currentText.text += value.text
await updatePart(currentText)
}
break
case "text-end":
if (currentText && currentText.text) {
currentText.time = {
start: Date.now(),
end: Date.now(),
}
await updatePart(currentText)
}
currentText = undefined
break
case "finish":
assistantMsg.time.completed = Date.now()
await updateMessage(assistantMsg)
break
default:
log.info("unhandled", {
...value,
})
continue
} }
} const p = await parts(assistantMsg.sessionID, assistantMsg.id)
} catch (e) { for (const part of p) {
log.error("", { if (part.type === "tool" && part.state.status !== "completed") {
error: e, updatePart({
}) ...part,
switch (true) { state: {
case e instanceof DOMException && e.name === "AbortError": status: "error",
assistantMsg.error = new MessageV2.AbortedError( error: "Tool execution aborted",
{ message: e.message }, time: {
{ start: Date.now(),
cause: e, end: Date.now(),
}, },
).toObject() input: {},
break },
case MessageV2.OutputLengthError.isInstance(e): })
assistantMsg.error = e }
break }
case LoadAPIKeyError.isInstance(e): assistantMsg.time.completed = Date.now()
assistantMsg.error = new Provider.AuthError( await updateMessage(assistantMsg)
{ return { info: assistantMsg, parts: p }
providerID: model.id, },
message: e.message,
},
{ cause: e },
).toObject()
break
case e instanceof Error:
assistantMsg.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
break
default:
assistantMsg.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
}
Bus.publish(Event.Error, {
sessionID: assistantMsg.sessionID,
error: assistantMsg.error,
})
} }
const p = await parts(assistantMsg.sessionID, assistantMsg.id)
for (const part of p) {
if (part.type === "tool" && part.state.status !== "completed") {
updatePart({
...part,
state: {
status: "error",
error: "Tool execution aborted",
time: {
start: Date.now(),
end: Date.now(),
},
input: {},
},
})
}
}
assistantMsg.time.completed = Date.now()
await updateMessage(assistantMsg)
return { info: assistantMsg, parts: p }
} }
export async function revert(_input: { sessionID: string; messageID: string; part: number }) { export async function revert(_input: { sessionID: string; messageID: string; part: number }) {
@@ -980,7 +1013,9 @@ export namespace Session {
} }
await updateMessage(next) await updateMessage(next)
const processor = createProcessor(next, model.info)
const stream = streamText({ const stream = streamText({
maxRetries: 10,
abortSignal: abort.signal, abortSignal: abort.signal,
model: model.language, model: model.language,
messages: [ messages: [
@@ -1003,7 +1038,7 @@ export namespace Session {
], ],
}) })
const result = await processStream(next, model.info, stream) const result = await processor.process(stream)
return result return result
} }
+17 -3
View File
@@ -1,6 +1,5 @@
import z from "zod" import z from "zod"
import { Bus } from "../bus" import { Bus } from "../bus"
import { Provider } from "../provider/provider"
import { NamedError } from "../util/error" import { NamedError } from "../util/error"
import { Message } from "./message" import { Message } from "./message"
import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai" import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
@@ -9,6 +8,13 @@ import { Identifier } from "../id/id"
export namespace MessageV2 { export namespace MessageV2 {
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
export const AbortedError = NamedError.create("MessageAbortedError", z.object({})) export const AbortedError = NamedError.create("MessageAbortedError", z.object({}))
export const AuthError = NamedError.create(
"ProviderAuthError",
z.object({
providerID: z.string(),
message: z.string(),
}),
)
export const ToolStatePending = z export const ToolStatePending = z
.object({ .object({
@@ -79,6 +85,14 @@ export namespace MessageV2 {
messageID: z.string(), messageID: z.string(),
}) })
export const SnapshotPart = PartBase.extend({
type: z.literal("snapshot"),
snapshot: z.string(),
}).openapi({
ref: "SnapshotPart",
})
export type SnapshotPart = z.infer<typeof SnapshotPart>
export const TextPart = PartBase.extend({ export const TextPart = PartBase.extend({
type: z.literal("text"), type: z.literal("text"),
text: z.string(), text: z.string(),
@@ -154,7 +168,7 @@ export namespace MessageV2 {
export type User = z.infer<typeof User> export type User = z.infer<typeof User>
export const Part = z export const Part = z
.discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart]) .discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart])
.openapi({ .openapi({
ref: "Part", ref: "Part",
}) })
@@ -168,7 +182,7 @@ export namespace MessageV2 {
}), }),
error: z error: z
.discriminatedUnion("name", [ .discriminatedUnion("name", [
Provider.AuthError.Schema, AuthError.Schema,
NamedError.Unknown.Schema, NamedError.Unknown.Schema,
OutputLengthError.Schema, OutputLengthError.Schema,
AbortedError.Schema, AbortedError.Schema,
+8 -6
View File
@@ -1,9 +1,15 @@
import z from "zod" import z from "zod"
import { Provider } from "../provider/provider"
import { NamedError } from "../util/error" import { NamedError } from "../util/error"
export namespace Message { export namespace Message {
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
export const AuthError = NamedError.create(
"ProviderAuthError",
z.object({
providerID: z.string(),
message: z.string(),
}),
)
export const ToolCall = z export const ToolCall = z
.object({ .object({
@@ -134,11 +140,7 @@ export namespace Message {
completed: z.number().optional(), completed: z.number().optional(),
}), }),
error: z error: z
.discriminatedUnion("name", [ .discriminatedUnion("name", [AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema])
Provider.AuthError.Schema,
NamedError.Unknown.Schema,
OutputLengthError.Schema,
])
.optional(), .optional(),
sessionID: z.string(), sessionID: z.string(),
tool: z.record( tool: z.record(
@@ -0,0 +1,155 @@
You are opencode, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and efficiently, adhering strictly to the following instructions and utilizing your available tools.
# Core Mandates
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
- **Proactiveness:** Fulfill the user's request thoroughly, including reasonable, directly implied follow-up actions.
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
- **Path Construction:** Before using any file system tool (e.g., read' or 'write'), you must construct the full absolute path for the file_path argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path.
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
# Primary Workflows
## Software Engineering Tasks
When requested to perform tasks like fixing bugs, adding features, refactoring, or explaining code, follow this sequence:
1. **Understand:** Think about the user's request and the relevant codebase context. Use 'grep' and 'glob' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use 'read' to understand context and validate any assumptions you may have.
2. **Plan:** Build a coherent and grounded (based on the understanding in step 1) plan for how you intend to resolve the user's task. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should try to use a self-verification loop by writing unit tests if relevant to the task. Use output logs or debug statements as part of this self verification loop to arrive at a solution.
3. **Implement:** Use the available tools (e.g., 'edit', 'write' 'bash' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates').
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands.
5. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. If unsure about these commands, you can ask the user if they'd like you to run them and if so how to.
## New Applications
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype. Utilize all tools at your disposal to implement the application. Some tools you may especially find useful are 'write', 'edit' and 'bash'.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. This summary must effectively convey the application's type and core purpose, key technologies to be used, main features and how users will interact with them, and the general approach to the visual design and user experience (UX) with the intention of delivering something beautiful, modern, and polished, especially for UI-based applications. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns, or open-source assets if feasible and licenses permit) to ensure a visually complete initial prototype. Ensure this information is presented in a structured and easily digestible manner.
3. **User Approval:** Obtain user approval for the proposed plan.
4. **Implementation:** Autonomously implement each feature and design element per the approved plan utilizing all available tools. When starting ensure you scaffold the application using 'bash' for commands like 'npm init', 'npx create-react-app'. Aim for full scope completion. Proactively create or source necessary placeholder assets (e.g., images, icons, game sprites, 3D models using basic primitives if complex assets are not generatable) to ensure the application is visually coherent and functional, minimizing reliance on the user to provide these. If the model can generate simple assets (e.g., a uniformly colored square sprite, a simple 3D cube), it should do so. Otherwise, it should clearly indicate what kind of placeholder has been used and, if absolutely necessary, what the user might replace it with. Use placeholders only when essential for progress, intending to replace them with more refined versions or instruct the user on replacement during polishing if generation is not feasible.
5. **Verify:** Review work against the original request, the approved plan. Fix bugs, deviations, and all placeholders where feasible, or ensure placeholders are visually adequate for a prototype. Ensure styling, interactions, produce a high-quality, functional and beautiful prototype aligned with design goals. Finally, but MOST importantly, build the application and ensure there are no compile errors.
6. **Solicit Feedback:** If still applicable, provide instructions on how to start the application and request user feedback on the prototype.
# Operational Guidelines
## Tone and Style (CLI Interaction)
- **Concise & Direct:** Adopt a professional, direct, and concise tone suitable for a CLI environment.
- **Minimal Output:** Aim for fewer than 3 lines of text output (excluding tool use/code generation) per response whenever practical. Focus strictly on the user's query.
- **Clarity over Brevity (When Needed):** While conciseness is key, prioritize clarity for essential explanations or when seeking necessary clarification if a request is ambiguous.
- **No Chitchat:** Avoid conversational filler, preambles ("Okay, I will now..."), or postambles ("I have finished the changes..."). Get straight to the action or answer.
- **Formatting:** Use GitHub-flavored Markdown. Responses will be rendered in monospace.
- **Tools vs. Text:** Use tools for actions, text output *only* for communication. Do not add explanatory comments within tool calls or code blocks unless specifically part of the required code/command itself.
- **Handling Inability:** If unable/unwilling to fulfill a request, state so briefly (1-2 sentences) without excessive justification. Offer alternatives if appropriate.
## Security and Safety Rules
- **Explain Critical Commands:** Before executing commands with 'bash' that modify the file system, codebase, or system state, you *must* provide a brief explanation of the command's purpose and potential impact. Prioritize user understanding and safety. You should not ask permission to use the tool; the user will be presented with a confirmation dialogue upon use (you do not need to tell them this).
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
## Tool Usage
- **File Paths:** Always use absolute paths when referring to files with tools like 'read' or 'write'. Relative paths are not supported. You must provide an absolute path.
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
- **Command Execution:** Use the 'bash' tool for running shell commands, remembering the safety rule to explain modifying commands first.
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
## Interaction Details
- **Help Command:** The user can use '/help' to display help information.
- **Feedback:** To report a bug or provide feedback, please use the /bug command.
# Examples (Illustrating Tone and Workflow)
<example>
user: 1 + 2
model: 3
</example>
<example>
user: is 13 a prime number?
model: true
</example>
<example>
user: list files here.
model: [tool_call: ls for path '/path/to/project']
</example>
<example>
user: start the server implemented in server.js
model: [tool_call: bash for 'node server.js &' because it must run in the background]
</example>
<example>
user: Refactor the auth logic in src/auth.py to use the requests library instead of urllib.
model: Okay, I can refactor 'src/auth.py'.
First, I'll analyze the code and check for a test safety net before planning any changes.
[tool_call: glob for path 'tests/test_auth.py']
[tool_call: read for absolute_path '/path/to/tests/test_auth.py']
(After analysis)
Great, 'tests/test_auth.py' exists and covers the core authentication logic. With this safety net in place, I can safely plan the refactoring.
I'll also confirm 'requests' is a dependency.
[tool_call: read for absolute_path '/path/to/requirements.txt']
(After analysis)
Looks good, 'requests' is available.
Here's the plan:
1. Replace the 'urllib' calls with 'requests'.
2. Add proper 'try...except' error handling for the new network calls.
3. Remove the old 'urllib' import.
4. Run the project's linter and tests to verify the changes.
Should I proceed?
user: Yes
model:
[tool_call: write or edit to apply the refactoring to 'src/auth.py']
Refactoring complete. Running verification...
[tool_call: bash for 'ruff check src/auth.py && pytest']
(After verification passes)
All checks passed. This is a stable checkpoint.
<example>
user: Delete the temp directory.
model: I can run \`rm -rf /path/to/project/temp\`. This will permanently delete the directory and all its contents.
</example>
<example>
user: Write tests for someFile.ts
model:
Okay, I can write those tests. First, I'll read \`someFile.ts\` to understand its functionality.
[tool_call: read for absolute_path '/path/to/someFile.ts' or use glob to find \`someFile.ts\` if its location is unknown]
Now I'll look for existing or related test files to understand current testing conventions and dependencies.
[tool_call: glob for paths ['**/*.test.ts', 'src/**/*.spec.ts'] assuming someFile.ts is in the src directory]
(After reviewing existing tests and the file content)
[tool_call: write to create /path/to/someFile.test.ts with the test code]
I've written the tests. Now I'll run the project's test command to verify them.
[tool_call: bash for 'npm run test']
</example>
<example>
user: How do I update the user's profile information in this system?
model:
I'm not immediately sure how user profile information is updated. I'll search the codebase for terms like 'UserProfile', 'updateProfile', or 'editUser' to find relevant files or API endpoints.
[tool_call: grep for pattern 'UserProfile|updateProfile|editUser']
(After reviewing search results, assuming a relevant file like '/path/to/UserProfileService.java' was found)
Okay, \`/path/to/UserProfileService.java\` seems like the most relevant file. I'll read its content to understand how updates are handled.
[tool_call: read for absolute_path '/path/to/UserProfileService.java']
(After reading the file)
It appears the \`updateUserProfile\` method in \`UserProfileService.java\` is responsible for this. It expects a user ID and a \`UserProfileDTO\` object...
</example>
<example>
user: Where are all the 'app.config' files in this project? I need to check their settings.
model:
[tool_call: glob for pattern '**/app.config']
(Assuming GlobTool returns a list of paths like ['/path/to/moduleA/app.config', '/path/to/moduleB/app.config'])
I found the following 'app.config' files:
- /path/to/moduleA/app.config
- /path/to/moduleB/app.config
To help you check their settings, I can read their contents. Which one would you like to start with, or should I read all of them?
</example>
# Final Reminder
Your core function is efficient and safe assistance. Balance extreme conciseness with the crucial need for clarity, especially regarding safety and potential system modifications. Always prioritize user control and project conventions. Never make assumptions about the contents of files; instead use 'read' to ensure you aren't making broad assumptions. Finally, you are an agent - please keep going until the user's query is completely resolved.
+4 -3
View File
@@ -8,14 +8,15 @@ import os from "os"
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt" import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
import PROMPT_BEAST from "./prompt/beast.txt" import PROMPT_BEAST from "./prompt/beast.txt"
import PROMPT_GEMINI from "./prompt/gemini.txt"
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt" import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
import PROMPT_SUMMARIZE from "./prompt/summarize.txt" import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
import PROMPT_TITLE from "./prompt/title.txt" import PROMPT_TITLE from "./prompt/title.txt"
export namespace SystemPrompt { export namespace SystemPrompt {
export function provider(providerID: string, modelID: string) { export function provider(modelID: string) {
if (providerID === "anthropic") return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_ANTHROPIC] if (modelID.includes("gpt-") || modelID.includes("o1") || modelID.includes("o3")) return [PROMPT_BEAST]
if (modelID.includes("gpt-")) return [PROMPT_BEAST] if (modelID.includes("gemini-")) return [PROMPT_GEMINI]
return [PROMPT_ANTHROPIC] return [PROMPT_ANTHROPIC]
} }
+15 -12
View File
@@ -9,10 +9,8 @@ export namespace Snapshot {
const log = Log.create({ service: "snapshot" }) const log = Log.create({ service: "snapshot" })
export async function create(sessionID: string) { export async function create(sessionID: string) {
return
log.info("creating snapshot") log.info("creating snapshot")
const app = App.info() const app = App.info()
const git = gitdir(sessionID)
// not a git repo, check if too big to snapshot // not a git repo, check if too big to snapshot
if (!app.git) { if (!app.git) {
@@ -21,9 +19,10 @@ export namespace Snapshot {
limit: 1000, limit: 1000,
}) })
log.info("found files", { count: files.length }) log.info("found files", { count: files.length })
if (files.length > 1000) return if (files.length >= 1000) return
} }
const git = gitdir(sessionID)
if (await fs.mkdir(git, { recursive: true })) { if (await fs.mkdir(git, { recursive: true })) {
await $`git init` await $`git init`
.env({ .env({
@@ -39,23 +38,27 @@ export namespace Snapshot {
await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow() await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow()
log.info("added files") log.info("added files")
const result = const result = await $`git --git-dir ${git} commit -m "snapshot" --author="opencode <mail@opencode.ai>"`
await $`git --git-dir ${git} commit --allow-empty -m "snapshot" --author="opencode <mail@opencode.ai>"` .quiet()
.quiet() .cwd(app.path.cwd)
.cwd(app.path.cwd) .nothrow()
.nothrow()
log.info("commit")
const match = result.stdout.toString().match(/\[.+ ([a-f0-9]+)\]/) const match = result.stdout.toString().match(/\[.+ ([a-f0-9]+)\]/)
if (!match) return if (!match) return
return match![1] return match![1]
} }
export async function restore(sessionID: string, commit: string) { export async function restore(sessionID: string, snapshot: string) {
log.info("restore", { commit }) log.info("restore", { commit: snapshot })
const app = App.info() const app = App.info()
const git = gitdir(sessionID) const git = gitdir(sessionID)
await $`git --git-dir=${git} checkout ${commit} --force`.quiet().cwd(app.path.root) await $`git --git-dir=${git} checkout ${snapshot} --force`.quiet().cwd(app.path.root)
}
export async function diff(sessionID: string, commit: string) {
const git = gitdir(sessionID)
const result = await $`git --git-dir=${git} diff -R ${commit}`.quiet().cwd(App.info().path.root)
return result.stdout.toString("utf8")
} }
function gitdir(sessionID: string) { function gitdir(sessionID: string) {
+10 -9
View File
@@ -121,18 +121,19 @@ export namespace Storage {
} }
const glob = new Bun.Glob("**/*") const glob = new Bun.Glob("**/*")
export async function* list(prefix: string) { export async function list(prefix: string) {
const dir = await state().then((x) => x.dir) const dir = await state().then((x) => x.dir)
try { try {
for await (const item of glob.scan({ const result = await Array.fromAsync(
cwd: path.join(dir, prefix), glob.scan({
onlyFiles: true, cwd: path.join(dir, prefix),
})) { onlyFiles: true,
const result = path.join(prefix, item.slice(0, -5)) }),
yield result ).then((items) => items.map((item) => path.join(prefix, item.slice(0, -5))))
} result.sort()
return result
} catch { } catch {
return return []
} }
} }
} }
+2 -2
View File
@@ -20,8 +20,8 @@ export const EditTool = Tool.define({
parameters: z.object({ parameters: z.object({
filePath: z.string().describe("The absolute path to the file to modify"), filePath: z.string().describe("The absolute path to the file to modify"),
oldString: z.string().describe("The text to replace"), oldString: z.string().describe("The text to replace"),
newString: z.string().describe("The text to replace it with (must be different from old_string)"), newString: z.string().describe("The text to replace it with (must be different from oldString)"),
replaceAll: z.boolean().optional().describe("Replace all occurrences of old_string (default false)"), replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
if (!params.filePath) { if (!params.filePath) {
+3 -3
View File
@@ -2,8 +2,8 @@ Performs exact string replacements in files.
Usage: Usage:
- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file. - You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string. - When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the oldString or newString.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked. - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
- The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`. - The edit will FAIL if `oldString` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replaceAll` to change every instance of `oldString`.
- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance. - Use `replaceAll` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
+7 -7
View File
@@ -8,9 +8,9 @@ Before using this tool:
To make multiple file edits, provide the following: To make multiple file edits, provide the following:
1. file_path: The absolute path to the file to modify (must be absolute, not relative) 1. file_path: The absolute path to the file to modify (must be absolute, not relative)
2. edits: An array of edit operations to perform, where each edit contains: 2. edits: An array of edit operations to perform, where each edit contains:
- old_string: The text to replace (must match the file contents exactly, including all whitespace and indentation) - oldString: The text to replace (must match the file contents exactly, including all whitespace and indentation)
- new_string: The edited text to replace the old_string - newString: The edited text to replace the oldString
- replace_all: Replace all occurrences of old_string. This parameter is optional and defaults to false. - replaceAll: Replace all occurrences of oldString. This parameter is optional and defaults to false.
IMPORTANT: IMPORTANT:
- All edits are applied in sequence, in the order they are provided - All edits are applied in sequence, in the order they are provided
@@ -24,8 +24,8 @@ CRITICAL REQUIREMENTS:
3. Plan your edits carefully to avoid conflicts between sequential operations 3. Plan your edits carefully to avoid conflicts between sequential operations
WARNING: WARNING:
- The tool will fail if edits.old_string doesn't match the file contents exactly (including whitespace) - The tool will fail if edits.oldString doesn't match the file contents exactly (including whitespace)
- The tool will fail if edits.old_string and edits.new_string are the same - The tool will fail if edits.oldString and edits.newString are the same
- Since edits are applied in sequence, ensure that earlier edits don't affect the text that later edits are trying to find - Since edits are applied in sequence, ensure that earlier edits don't affect the text that later edits are trying to find
When making edits: When making edits:
@@ -33,9 +33,9 @@ When making edits:
- Do not leave the code in a broken state - Do not leave the code in a broken state
- Always use absolute file paths (starting with /) - Always use absolute file paths (starting with /)
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked. - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
- Use replace_all for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance. - Use replaceAll for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
If you want to create a new file, use: If you want to create a new file, use:
- A new file path, including dir name if needed - A new file path, including dir name if needed
- First edit: empty old_string and the new file's contents as new_string - First edit: empty oldString and the new file's contents as newString
- Subsequent edits: normal edit operations on the created content - Subsequent edits: normal edit operations on the created content
-4
View File
@@ -7,7 +7,6 @@ import { FileTime } from "../file/time"
import DESCRIPTION from "./read.txt" import DESCRIPTION from "./read.txt"
import { App } from "../app/app" import { App } from "../app/app"
const MAX_READ_SIZE = 250 * 1024
const DEFAULT_READ_LIMIT = 2000 const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000 const MAX_LINE_LENGTH = 2000
@@ -45,10 +44,7 @@ export const ReadTool = Tool.define({
throw new Error(`File not found: ${filePath}`) throw new Error(`File not found: ${filePath}`)
} }
const stats = await file.stat()
if (stats.size > MAX_READ_SIZE)
throw new Error(`File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`)
const limit = params.limit ?? DEFAULT_READ_LIMIT const limit = params.limit ?? DEFAULT_READ_LIMIT
const offset = params.offset || 0 const offset = params.offset || 0
const isImage = isImageFile(filePath) const isImage = isImageFile(filePath)
+7 -14
View File
@@ -15,21 +15,15 @@ export const TaskTool = Tool.define({
}), }),
async execute(params, ctx) { async execute(params, ctx) {
const session = await Session.create(ctx.sessionID) const session = await Session.create(ctx.sessionID)
const msg = (await Session.getMessage(ctx.sessionID, ctx.messageID)) as MessageV2.Assistant const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
if (msg.role !== "assistant") throw new Error("Not an assistant message")
const parts: Record<string, MessageV2.Part> = {}
function summary(input: MessageV2.Part[]) {
const result = []
for (const part of input) {
if (part.type === "tool" && part.state.status === "completed") {
result.push(part)
}
}
return result
}
const messageID = Identifier.ascending("message")
const parts: Record<string, MessageV2.ToolPart> = {}
const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => { const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
if (evt.properties.part.sessionID !== session.id) return if (evt.properties.part.sessionID !== session.id) return
if (evt.properties.part.messageID === messageID) return
if (evt.properties.part.type !== "tool") return
parts[evt.properties.part.id] = evt.properties.part parts[evt.properties.part.id] = evt.properties.part
ctx.metadata({ ctx.metadata({
title: params.description, title: params.description,
@@ -42,7 +36,6 @@ export const TaskTool = Tool.define({
ctx.abort.addEventListener("abort", () => { ctx.abort.addEventListener("abort", () => {
Session.abort(session.id) Session.abort(session.id)
}) })
const messageID = Identifier.ascending("message")
const result = await Session.chat({ const result = await Session.chat({
messageID, messageID,
sessionID: session.id, sessionID: session.id,
@@ -62,7 +55,7 @@ export const TaskTool = Tool.define({
return { return {
title: params.description, title: params.description,
metadata: { metadata: {
summary: summary(result.parts), summary: result.parts.filter((x) => x.type === "tool"),
}, },
output: result.parts.findLast((x) => x.type === "text")!.text, output: result.parts.findLast((x) => x.type === "text")!.text,
} }
-25
View File
@@ -1,25 +0,0 @@
# TUI Agent Guidelines
## Build/Test Commands
- **Build**: `go build ./cmd/opencode` (builds main binary)
- **Test**: `go test ./...` (runs all tests)
- **Single test**: `go test ./internal/theme -run TestLoadThemesFromJSON` (specific test)
- **Release build**: Uses `.goreleaser.yml` configuration
## Code Style
- **Language**: Go 1.24+ with standard formatting (`gofmt`)
- **Imports**: Group standard, third-party, local packages with blank lines
- **Naming**: Go conventions - PascalCase exports, camelCase private, ALL_CAPS constants
- **Error handling**: Return errors explicitly, use `fmt.Errorf` for wrapping
- **Structs**: Define clear interfaces, embed when appropriate
- **Testing**: Use table-driven tests, `t.TempDir()` for file operations
## Architecture
- **TUI Framework**: Bubble Tea v2 with Lipgloss v2 for styling
- **Client**: Generated OpenAPI client communicates with TypeScript server
- **Components**: Reusable UI components in `internal/components/`
- **Themes**: JSON-based theming system with override hierarchy
- **State**: Centralized app state with message passing
+66 -70
View File
@@ -31,42 +31,20 @@ type win32InputState struct {
// Reader represents an input event reader. It reads input events and parses // Reader represents an input event reader. It reads input events and parses
// escape sequences from the terminal input buffer and translates them into // escape sequences from the terminal input buffer and translates them into
// human-readable events. // humanreadable events.
type Reader struct { type Reader struct {
rd cancelreader.CancelReader rd cancelreader.CancelReader
table map[string]Key // table is a lookup table for key sequences. table map[string]Key // table is a lookup table for key sequences.
term string // $TERM
term string // term is the terminal name $TERM. paste []byte // bracketed paste buffer; nil when disabled
buf [256]byte // read buffer
// paste is the bracketed paste mode buffer. partialSeq []byte // holds incomplete escape sequences
// When nil, bracketed paste mode is disabled. keyState win32InputState
paste []byte parser Parser
logger Logger
buf [256]byte // do we need a larger buffer?
// partialSeq holds incomplete escape sequences that need more data
partialSeq []byte
// keyState keeps track of the current Windows Console API key events state.
// It is used to decode ANSI escape sequences and utf16 sequences.
keyState win32InputState
parser Parser
logger Logger
} }
// NewReader returns a new input event reader. The reader reads input events // NewReader returns a new input event reader.
// from the terminal and parses escape sequences into human-readable events. It
// supports reading Terminfo databases. See [Parser] for more information.
//
// Example:
//
// r, _ := input.NewReader(os.Stdin, os.Getenv("TERM"), 0)
// defer r.Close()
// events, _ := r.ReadEvents()
// for _, ev := range events {
// log.Printf("%v", ev)
// }
func NewReader(r io.Reader, termType string, flags int) (*Reader, error) { func NewReader(r io.Reader, termType string, flags int) (*Reader, error) {
d := new(Reader) d := new(Reader)
cr, err := newCancelreader(r, flags) cr, err := newCancelreader(r, flags)
@@ -82,46 +60,38 @@ func NewReader(r io.Reader, termType string, flags int) (*Reader, error) {
} }
// SetLogger sets a logger for the reader. // SetLogger sets a logger for the reader.
func (d *Reader) SetLogger(l Logger) { func (d *Reader) SetLogger(l Logger) { d.logger = l }
d.logger = l
}
// Read implements [io.Reader]. // Read implements io.Reader.
func (d *Reader) Read(p []byte) (int, error) { func (d *Reader) Read(p []byte) (int, error) { return d.rd.Read(p) }
return d.rd.Read(p) //nolint:wrapcheck
}
// Cancel cancels the underlying reader. // Cancel cancels the underlying reader.
func (d *Reader) Cancel() bool { func (d *Reader) Cancel() bool { return d.rd.Cancel() }
return d.rd.Cancel()
}
// Close closes the underlying reader. // Close closes the underlying reader.
func (d *Reader) Close() error { func (d *Reader) Close() error { return d.rd.Close() }
return d.rd.Close() //nolint:wrapcheck
}
func (d *Reader) readEvents() ([]Event, error) { func (d *Reader) readEvents() ([]Event, error) {
nb, err := d.rd.Read(d.buf[:]) nb, err := d.rd.Read(d.buf[:])
if err != nil { if err != nil {
return nil, err //nolint:wrapcheck return nil, err
} }
var events []Event var events []Event
// Combine any partial sequence from previous read with new data // Combine any partial sequence from previous read with new data.
var buf []byte var buf []byte
if len(d.partialSeq) > 0 { if len(d.partialSeq) > 0 {
buf = make([]byte, len(d.partialSeq)+nb) buf = make([]byte, len(d.partialSeq)+nb)
copy(buf, d.partialSeq) copy(buf, d.partialSeq)
copy(buf[len(d.partialSeq):], d.buf[:nb]) copy(buf[len(d.partialSeq):], d.buf[:nb])
d.partialSeq = nil // clear the partial sequence d.partialSeq = nil
} else { } else {
buf = d.buf[:nb] buf = d.buf[:nb]
} }
// Lookup table first // Fast path: direct lookup for simple escape sequences.
if bytes.HasPrefix(buf, []byte{'\x1b'}) { if bytes.HasPrefix(buf, []byte{0x1b}) {
if k, ok := d.table[string(buf)]; ok { if k, ok := d.table[string(buf)]; ok {
if d.logger != nil { if d.logger != nil {
d.logger.Printf("input: %q", buf) d.logger.Printf("input: %q", buf)
@@ -133,24 +103,23 @@ func (d *Reader) readEvents() ([]Event, error) {
var i int var i int
for i < len(buf) { for i < len(buf) {
nb, ev := d.parser.parseSequence(buf[i:]) consumed, ev := d.parser.parseSequence(buf[i:])
if d.logger != nil && nb > 0 { if d.logger != nil && consumed > 0 {
d.logger.Printf("input: %q", buf[i:i+nb]) d.logger.Printf("input: %q", buf[i:i+consumed])
} }
// Handle incomplete sequences - when parseSequence returns (0, nil) // Incomplete sequence store remainder and exit.
// it means we need more data to complete the sequence if consumed == 0 && ev == nil {
if nb == 0 && ev == nil { rem := len(buf) - i
// Store the remaining data for the next read if rem > 0 {
remaining := len(buf) - i d.partialSeq = make([]byte, rem)
if remaining > 0 {
d.partialSeq = make([]byte, remaining)
copy(d.partialSeq, buf[i:]) copy(d.partialSeq, buf[i:])
} }
break break
} }
// Handle bracketed-paste // Handle bracketed paste specially so we dont emit a paste event for
// every byte.
if d.paste != nil { if d.paste != nil {
if _, ok := ev.(PasteEndEvent); !ok { if _, ok := ev.(PasteEndEvent); !ok {
d.paste = append(d.paste, buf[i]) d.paste = append(d.paste, buf[i])
@@ -160,15 +129,9 @@ func (d *Reader) readEvents() ([]Event, error) {
} }
switch ev.(type) { switch ev.(type) {
// case UnknownEvent:
// // If the sequence is not recognized by the parser, try looking it up.
// if k, ok := d.table[string(buf[i:i+nb])]; ok {
// ev = KeyPressEvent(k)
// }
case PasteStartEvent: case PasteStartEvent:
d.paste = []byte{} d.paste = []byte{}
case PasteEndEvent: case PasteEndEvent:
// Decode the captured data into runes.
var paste []rune var paste []rune
for len(d.paste) > 0 { for len(d.paste) > 0 {
r, w := utf8.DecodeRune(d.paste) r, w := utf8.DecodeRune(d.paste)
@@ -177,7 +140,7 @@ func (d *Reader) readEvents() ([]Event, error) {
} }
d.paste = d.paste[w:] d.paste = d.paste[w:]
} }
d.paste = nil // reset the buffer d.paste = nil
events = append(events, PasteEvent(paste)) events = append(events, PasteEvent(paste))
case nil: case nil:
i++ i++
@@ -189,8 +152,41 @@ func (d *Reader) readEvents() ([]Event, error) {
} else { } else {
events = append(events, ev) events = append(events, ev)
} }
i += nb i += consumed
} }
// Collapse bursts of wheel/motion events into a single event each.
events = coalesceMouseEvents(events)
return events, nil return events, nil
} }
// coalesceMouseEvents reduces the volume of MouseWheelEvent and MouseMotionEvent
// objects that arrive in rapid succession by keeping only the most recent
// event in each contiguous run.
func coalesceMouseEvents(in []Event) []Event {
if len(in) < 2 {
return in
}
out := make([]Event, 0, len(in))
for _, ev := range in {
switch ev.(type) {
case MouseWheelEvent:
if len(out) > 0 {
if _, ok := out[len(out)-1].(MouseWheelEvent); ok {
out[len(out)-1] = ev // replace previous wheel event
continue
}
}
case MouseMotionEvent:
if len(out) > 0 {
if _, ok := out[len(out)-1].(MouseMotionEvent); ok {
out[len(out)-1] = ev // replace previous motion event
continue
}
}
}
out = append(out, ev)
}
return out
}
+2 -1
View File
@@ -303,7 +303,8 @@ func (p *Parser) parseCsi(b []byte) (int, Event) {
return i, CursorPositionEvent{Y: row - 1, X: col - 1} return i, CursorPositionEvent{Y: row - 1, X: col - 1}
case 'm' | '<'<<parser.PrefixShift, 'M' | '<'<<parser.PrefixShift: case 'm' | '<'<<parser.PrefixShift, 'M' | '<'<<parser.PrefixShift:
// Handle SGR mouse // Handle SGR mouse
if paramsLen == 3 { if paramsLen >= 3 {
pa = pa[:3]
return i, parseSGRMouseEvent(cmd, pa) return i, parseSGRMouseEvent(cmd, pa)
} }
case 'm' | '>'<<parser.PrefixShift: case 'm' | '>'<<parser.PrefixShift:
+5 -2
View File
@@ -50,6 +50,9 @@ type App struct {
IsLeaderSequence bool IsLeaderSequence bool
} }
type SessionCreatedMsg = struct {
Session *opencode.Session
}
type SessionSelectedMsg = *opencode.Session type SessionSelectedMsg = *opencode.Session
type SessionLoadedMsg struct{} type SessionLoadedMsg struct{}
type ModelSelectedMsg struct { type ModelSelectedMsg struct {
@@ -380,7 +383,7 @@ func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
} }
a.Session = session a.Session = session
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session))) cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
go func() { go func() {
_, err := a.Client.Session.Init(ctx, a.Session.ID, opencode.SessionInitParams{ _, err := a.Client.Session.Init(ctx, a.Session.ID, opencode.SessionInitParams{
@@ -456,7 +459,7 @@ func (a *App) SendChatMessage(
return a, toast.NewErrorToast(err.Error()) return a, toast.NewErrorToast(err.Error())
} }
a.Session = session a.Session = session
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session))) cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
} }
message := opencode.UserMessage{ message := opencode.UserMessage{
@@ -94,6 +94,7 @@ const (
SessionUnshareCommand CommandName = "session_unshare" SessionUnshareCommand CommandName = "session_unshare"
SessionInterruptCommand CommandName = "session_interrupt" SessionInterruptCommand CommandName = "session_interrupt"
SessionCompactCommand CommandName = "session_compact" SessionCompactCommand CommandName = "session_compact"
SessionExportCommand CommandName = "session_export"
ToolDetailsCommand CommandName = "tool_details" ToolDetailsCommand CommandName = "tool_details"
ModelListCommand CommandName = "model_list" ModelListCommand CommandName = "model_list"
ThemeListCommand CommandName = "theme_list" ThemeListCommand CommandName = "theme_list"
@@ -164,6 +165,12 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
Keybindings: parseBindings("<leader>e"), Keybindings: parseBindings("<leader>e"),
Trigger: []string{"editor"}, Trigger: []string{"editor"},
}, },
{
Name: SessionExportCommand,
Description: "export conversation",
Keybindings: parseBindings("<leader>x"),
Trigger: []string{"export"},
},
{ {
Name: SessionNewCommand, Name: SessionNewCommand,
Description: "new session", Description: "new session",
+24 -21
View File
@@ -8,7 +8,6 @@ import (
"github.com/lithammer/fuzzysearch/fuzzy" "github.com/lithammer/fuzzysearch/fuzzy"
"github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/commands" "github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
) )
@@ -17,7 +16,7 @@ type CommandCompletionProvider struct {
app *app.App app *app.App
} }
func NewCommandCompletionProvider(app *app.App) dialog.CompletionProvider { func NewCommandCompletionProvider(app *app.App) CompletionProvider {
return &CommandCompletionProvider{app: app} return &CommandCompletionProvider{app: app}
} }
@@ -32,24 +31,28 @@ func (c *CommandCompletionProvider) GetEmptyMessage() string {
func (c *CommandCompletionProvider) getCommandCompletionItem( func (c *CommandCompletionProvider) getCommandCompletionItem(
cmd commands.Command, cmd commands.Command,
space int, space int,
t theme.Theme, ) CompletionSuggestion {
) dialog.CompletionItemI { displayFunc := func(s styles.Style) string {
spacer := strings.Repeat(" ", space) t := theme.CurrentTheme()
title := " /" + cmd.PrimaryTrigger() + styles.NewStyle(). spacer := strings.Repeat(" ", space)
Foreground(t.TextMuted()). display := " /" + cmd.PrimaryTrigger() + s.
Render(spacer+cmd.Description) Foreground(t.TextMuted()).
Render(spacer+cmd.Description)
return display
}
value := string(cmd.Name) value := string(cmd.Name)
return dialog.NewCompletionItem(dialog.CompletionItem{ return CompletionSuggestion{
Title: title, Display: displayFunc,
Value: value, Value: value,
ProviderID: c.GetId(), ProviderID: c.GetId(),
}) RawData: cmd,
}
} }
func (c *CommandCompletionProvider) GetChildEntries( func (c *CommandCompletionProvider) GetChildEntries(
query string, query string,
) ([]dialog.CompletionItemI, error) { ) ([]CompletionSuggestion, error) {
t := theme.CurrentTheme()
commands := c.app.Commands commands := c.app.Commands
space := 1 space := 1
@@ -63,20 +66,20 @@ func (c *CommandCompletionProvider) GetChildEntries(
sorted := commands.Sorted() sorted := commands.Sorted()
if query == "" { if query == "" {
// If no query, return all commands // If no query, return all commands
items := []dialog.CompletionItemI{} items := []CompletionSuggestion{}
for _, cmd := range sorted { for _, cmd := range sorted {
if !cmd.HasTrigger() { if !cmd.HasTrigger() {
continue continue
} }
space := space - lipgloss.Width(cmd.PrimaryTrigger()) space := space - lipgloss.Width(cmd.PrimaryTrigger())
items = append(items, c.getCommandCompletionItem(cmd, space, t)) items = append(items, c.getCommandCompletionItem(cmd, space))
} }
return items, nil return items, nil
} }
// Use fuzzy matching for commands // Use fuzzy matching for commands
var commandNames []string var commandNames []string
commandMap := make(map[string]dialog.CompletionItemI) commandMap := make(map[string]CompletionSuggestion)
for _, cmd := range sorted { for _, cmd := range sorted {
if !cmd.HasTrigger() { if !cmd.HasTrigger() {
@@ -86,24 +89,24 @@ func (c *CommandCompletionProvider) GetChildEntries(
// Add all triggers as searchable options // Add all triggers as searchable options
for _, trigger := range cmd.Trigger { for _, trigger := range cmd.Trigger {
commandNames = append(commandNames, trigger) commandNames = append(commandNames, trigger)
commandMap[trigger] = c.getCommandCompletionItem(cmd, space, t) commandMap[trigger] = c.getCommandCompletionItem(cmd, space)
} }
} }
// Find fuzzy matches // Find fuzzy matches
matches := fuzzy.RankFind(query, commandNames) matches := fuzzy.RankFindFold(query, commandNames)
// Sort by score (best matches first) // Sort by score (best matches first)
sort.Sort(matches) sort.Sort(matches)
// Convert matches to completion items, deduplicating by command name // Convert matches to completion items, deduplicating by command name
items := []dialog.CompletionItemI{} items := []CompletionSuggestion{}
seen := make(map[string]bool) seen := make(map[string]bool)
for _, match := range matches { for _, match := range matches {
if item, ok := commandMap[match.Target]; ok { if item, ok := commandMap[match.Target]; ok {
// Use the command's value (name) as the deduplication key // Use the command's value (name) as the deduplication key
if !seen[item.GetValue()] { if !seen[item.Value] {
seen[item.GetValue()] = true seen[item.Value] = true
items = append(items, item) items = append(items, item)
} }
} }
+33 -30
View File
@@ -9,14 +9,13 @@ import (
"github.com/sst/opencode-sdk-go" "github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
) )
type filesContextGroup struct { type filesContextGroup struct {
app *app.App app *app.App
gitFiles []dialog.CompletionItemI gitFiles []CompletionSuggestion
} }
func (cg *filesContextGroup) GetId() string { func (cg *filesContextGroup) GetId() string {
@@ -27,12 +26,8 @@ func (cg *filesContextGroup) GetEmptyMessage() string {
return "no matching files" return "no matching files"
} }
func (cg *filesContextGroup) getGitFiles() []dialog.CompletionItemI { func (cg *filesContextGroup) getGitFiles() []CompletionSuggestion {
t := theme.CurrentTheme() items := make([]CompletionSuggestion, 0)
items := make([]dialog.CompletionItemI, 0)
base := styles.NewStyle().Background(t.BackgroundPanel())
green := base.Foreground(t.Success()).Render
red := base.Foreground(t.Error()).Render
status, _ := cg.app.Client.File.Status(context.Background()) status, _ := cg.app.Client.File.Status(context.Background())
if status != nil { if status != nil {
@@ -42,21 +37,25 @@ func (cg *filesContextGroup) getGitFiles() []dialog.CompletionItemI {
}) })
for _, file := range files { for _, file := range files {
title := file.Path displayFunc := func(s styles.Style) string {
if file.Added > 0 { t := theme.CurrentTheme()
title += green(" +" + strconv.Itoa(int(file.Added))) green := s.Foreground(t.Success()).Render
red := s.Foreground(t.Error()).Render
display := file.Path
if file.Added > 0 {
display += green(" +" + strconv.Itoa(int(file.Added)))
}
if file.Removed > 0 {
display += red(" -" + strconv.Itoa(int(file.Removed)))
}
return display
} }
if file.Removed > 0 { item := CompletionSuggestion{
title += red(" -" + strconv.Itoa(int(file.Removed))) Display: displayFunc,
}
item := dialog.NewCompletionItem(dialog.CompletionItem{
Title: title,
Value: file.Path, Value: file.Path,
ProviderID: cg.GetId(), ProviderID: cg.GetId(),
Raw: file, RawData: file,
}, }
dialog.WithBackgroundColor(t.BackgroundPanel()),
)
items = append(items, item) items = append(items, item)
} }
} }
@@ -66,8 +65,8 @@ func (cg *filesContextGroup) getGitFiles() []dialog.CompletionItemI {
func (cg *filesContextGroup) GetChildEntries( func (cg *filesContextGroup) GetChildEntries(
query string, query string,
) ([]dialog.CompletionItemI, error) { ) ([]CompletionSuggestion, error) {
items := make([]dialog.CompletionItemI, 0) items := make([]CompletionSuggestion, 0)
query = strings.TrimSpace(query) query = strings.TrimSpace(query)
if query == "" { if query == "" {
@@ -89,7 +88,7 @@ func (cg *filesContextGroup) GetChildEntries(
for _, file := range *files { for _, file := range *files {
exists := false exists := false
for _, existing := range cg.gitFiles { for _, existing := range cg.gitFiles {
if existing.GetValue() == file { if existing.Value == file {
if query != "" { if query != "" {
items = append(items, existing) items = append(items, existing)
} }
@@ -97,14 +96,18 @@ func (cg *filesContextGroup) GetChildEntries(
} }
} }
if !exists { if !exists {
item := dialog.NewCompletionItem(dialog.CompletionItem{ displayFunc := func(s styles.Style) string {
Title: file, // t := theme.CurrentTheme()
// return s.Foreground(t.Text()).Render(file)
return s.Render(file)
}
item := CompletionSuggestion{
Display: displayFunc,
Value: file, Value: file,
ProviderID: cg.GetId(), ProviderID: cg.GetId(),
Raw: file, RawData: file,
}, }
dialog.WithBackgroundColor(theme.CurrentTheme().BackgroundPanel()),
)
items = append(items, item) items = append(items, item)
} }
} }
@@ -112,7 +115,7 @@ func (cg *filesContextGroup) GetChildEntries(
return items, nil return items, nil
} }
func NewFileContextGroup(app *app.App) dialog.CompletionProvider { func NewFileContextGroup(app *app.App) CompletionProvider {
cg := &filesContextGroup{ cg := &filesContextGroup{
app: app, app: app,
} }
@@ -0,0 +1,8 @@
package completions
// CompletionProvider defines the interface for completion data providers
type CompletionProvider interface {
GetId() string
GetChildEntries(query string) ([]CompletionSuggestion, error)
GetEmptyMessage() string
}
@@ -0,0 +1,24 @@
package completions
import "github.com/sst/opencode/internal/styles"
// CompletionSuggestion represents a data-only completion suggestion
// with no styling or rendering logic
type CompletionSuggestion struct {
// The text to be displayed in the list. May contain minimal inline
// ANSI styling if intrinsic to the data (e.g., git diff colors).
Display func(styles.Style) string
// The value to be used when the item is selected (e.g., inserted into the editor).
Value string
// An optional, longer description to be displayed.
Description string
// The ID of the provider that generated this suggestion.
ProviderID string
// The raw, underlying data object (e.g., opencode.Symbol, commands.Command).
// This allows the selection handler to perform rich actions.
RawData any
}
+22 -21
View File
@@ -8,7 +8,6 @@ import (
"github.com/sst/opencode-sdk-go" "github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
) )
@@ -58,8 +57,8 @@ const (
func (cg *symbolsContextGroup) GetChildEntries( func (cg *symbolsContextGroup) GetChildEntries(
query string, query string,
) ([]dialog.CompletionItemI, error) { ) ([]CompletionSuggestion, error) {
items := make([]dialog.CompletionItemI, 0) items := make([]CompletionSuggestion, 0)
query = strings.TrimSpace(query) query = strings.TrimSpace(query)
if query == "" { if query == "" {
@@ -78,40 +77,42 @@ func (cg *symbolsContextGroup) GetChildEntries(
return items, nil return items, nil
} }
t := theme.CurrentTheme()
baseStyle := styles.NewStyle().Background(t.BackgroundElement())
base := baseStyle.Render
muted := baseStyle.Foreground(t.TextMuted()).Render
for _, sym := range *symbols { for _, sym := range *symbols {
parts := strings.Split(sym.Name, ".") parts := strings.Split(sym.Name, ".")
lastPart := parts[len(parts)-1] lastPart := parts[len(parts)-1]
title := base(lastPart)
uriParts := strings.Split(sym.Location.Uri, "/")
lastTwoParts := uriParts[len(uriParts)-2:]
joined := strings.Join(lastTwoParts, "/")
title += muted(fmt.Sprintf(" %s", joined))
start := int(sym.Location.Range.Start.Line) start := int(sym.Location.Range.Start.Line)
end := int(sym.Location.Range.End.Line) end := int(sym.Location.Range.End.Line)
title += muted(fmt.Sprintf(":L%d-%d", start, end))
displayFunc := func(s styles.Style) string {
t := theme.CurrentTheme()
base := s.Foreground(t.Text()).Render
muted := s.Foreground(t.TextMuted()).Render
display := base(lastPart)
uriParts := strings.Split(sym.Location.Uri, "/")
lastTwoParts := uriParts[len(uriParts)-2:]
joined := strings.Join(lastTwoParts, "/")
display += muted(fmt.Sprintf(" %s", joined))
display += muted(fmt.Sprintf(":L%d-%d", start, end))
return display
}
value := fmt.Sprintf("%s?start=%d&end=%d", sym.Location.Uri, start, end) value := fmt.Sprintf("%s?start=%d&end=%d", sym.Location.Uri, start, end)
item := dialog.NewCompletionItem(dialog.CompletionItem{ item := CompletionSuggestion{
Title: title, Display: displayFunc,
Value: value, Value: value,
ProviderID: cg.GetId(), ProviderID: cg.GetId(),
Raw: sym, RawData: sym,
}) }
items = append(items, item) items = append(items, item)
} }
return items, nil return items, nil
} }
func NewSymbolsContextGroup(app *app.App) dialog.CompletionProvider { func NewSymbolsContextGroup(app *app.App) CompletionProvider {
return &symbolsContextGroup{ return &symbolsContextGroup{
app: app, app: app,
} }
@@ -142,9 +142,9 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.spinner = createSpinner() m.spinner = createSpinner()
return m, tea.Batch(m.spinner.Tick, m.textarea.Focus()) return m, tea.Batch(m.spinner.Tick, m.textarea.Focus())
case dialog.CompletionSelectedMsg: case dialog.CompletionSelectedMsg:
switch msg.Item.GetProviderID() { switch msg.Item.ProviderID {
case "commands": case "commands":
commandName := strings.TrimPrefix(msg.Item.GetValue(), "/") commandName := strings.TrimPrefix(msg.Item.Value, "/")
updated, cmd := m.Clear() updated, cmd := m.Clear()
m = updated.(*editorComponent) m = updated.(*editorComponent)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
@@ -154,7 +154,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
atIndex := m.textarea.LastRuneIndex('@') atIndex := m.textarea.LastRuneIndex('@')
if atIndex == -1 { if atIndex == -1 {
// Should not happen, but as a fallback, just insert. // Should not happen, but as a fallback, just insert.
m.textarea.InsertString(msg.Item.GetValue() + " ") m.textarea.InsertString(msg.Item.Value + " ")
return m, nil return m, nil
} }
@@ -165,7 +165,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Now, insert the attachment at the position where the '@' was. // Now, insert the attachment at the position where the '@' was.
// The cursor is now at `atIndex` after the replacement. // The cursor is now at `atIndex` after the replacement.
filePath := msg.Item.GetValue() filePath := msg.Item.Value
extension := filepath.Ext(filePath) extension := filepath.Ext(filePath)
mediaType := "" mediaType := ""
switch extension { switch extension {
@@ -192,20 +192,20 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
atIndex := m.textarea.LastRuneIndex('@') atIndex := m.textarea.LastRuneIndex('@')
if atIndex == -1 { if atIndex == -1 {
// Should not happen, but as a fallback, just insert. // Should not happen, but as a fallback, just insert.
m.textarea.InsertString(msg.Item.GetValue() + " ") m.textarea.InsertString(msg.Item.Value + " ")
return m, nil return m, nil
} }
cursorCol := m.textarea.CursorColumn() cursorCol := m.textarea.CursorColumn()
m.textarea.ReplaceRange(atIndex, cursorCol, "") m.textarea.ReplaceRange(atIndex, cursorCol, "")
symbol := msg.Item.GetRaw().(opencode.Symbol) symbol := msg.Item.RawData.(opencode.Symbol)
parts := strings.Split(symbol.Name, ".") parts := strings.Split(symbol.Name, ".")
lastPart := parts[len(parts)-1] lastPart := parts[len(parts)-1]
attachment := &textarea.Attachment{ attachment := &textarea.Attachment{
ID: uuid.NewString(), ID: uuid.NewString(),
Display: "@" + lastPart, Display: "@" + lastPart,
URL: msg.Item.GetValue(), URL: msg.Item.Value,
Filename: lastPart, Filename: lastPart,
MediaType: "text/plain", MediaType: "text/plain",
} }
@@ -213,7 +213,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.textarea.InsertString(" ") m.textarea.InsertString(" ")
return m, nil return m, nil
default: default:
slog.Debug("Unknown provider", "provider", msg.Item.GetProviderID()) slog.Debug("Unknown provider", "provider", msg.Item.ProviderID)
return m, nil return m, nil
} }
} }
@@ -305,10 +305,8 @@ func renderToolDetails(
return "" return ""
} }
if toolCall.State.Status == opencode.ToolPartStateStatusPending || if toolCall.State.Status == opencode.ToolPartStateStatusPending {
toolCall.State.Status == opencode.ToolPartStateStatusRunning {
title := renderToolTitle(toolCall, width) title := renderToolTitle(toolCall, width)
title = styles.NewStyle().Width(width - 6).Render(title)
return renderContentBlock(app, title, highlight, width) return renderContentBlock(app, title, highlight, width)
} }
@@ -339,11 +337,14 @@ func renderToolDetails(
borderColor = t.BorderActive() borderColor = t.BorderActive()
} }
if toolCall.State.Status == opencode.ToolPartStateStatusCompleted { if toolCall.State.Metadata != nil {
metadata := toolCall.State.Metadata.(map[string]any) metadata := toolCall.State.Metadata.(map[string]any)
switch toolCall.Tool { switch toolCall.Tool {
case "read": case "read":
preview := metadata["preview"] var preview any
if metadata != nil {
preview = metadata["preview"]
}
if preview != nil && toolInputMap["filePath"] != nil { if preview != nil && toolInputMap["filePath"] != nil {
filename := toolInputMap["filePath"].(string) filename := toolInputMap["filePath"].(string)
body = preview.(string) body = preview.(string)
@@ -351,7 +352,10 @@ func renderToolDetails(
} }
case "edit": case "edit":
if filename, ok := toolInputMap["filePath"].(string); ok { if filename, ok := toolInputMap["filePath"].(string); ok {
diffField := metadata["diff"] var diffField any
if metadata != nil {
diffField = metadata["diff"]
}
if diffField != nil { if diffField != nil {
patch := diffField.(string) patch := diffField.(string)
var formattedDiff string var formattedDiff string
@@ -439,19 +443,17 @@ func renderToolDetails(
if summary != nil { if summary != nil {
toolcalls := summary.([]any) toolcalls := summary.([]any)
steps := []string{} steps := []string{}
for _, toolcall := range toolcalls { for _, item := range toolcalls {
call := toolcall.(map[string]any) data, _ := json.Marshal(item)
if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok { var toolCall opencode.ToolPart
data, _ := json.Marshal(toolInvocation) _ = json.Unmarshal(data, &toolCall)
var toolCall opencode.ToolPart step := renderToolTitle(toolCall, width)
_ = json.Unmarshal(data, &toolCall) step = "∟ " + step
step := renderToolTitle(toolCall, width) steps = append(steps, step)
step = "∟ " + step
steps = append(steps, step)
}
} }
body = strings.Join(steps, "\n") body = strings.Join(steps, "\n")
} }
body = styles.NewStyle().Width(width - 6).Render(body)
default: default:
if result == nil { if result == nil {
empty := "" empty := ""
@@ -539,10 +541,9 @@ func renderToolTitle(
toolCall opencode.ToolPart, toolCall opencode.ToolPart,
width int, width int,
) string { ) string {
// TODO: handle truncate to width
if toolCall.State.Status == opencode.ToolPartStateStatusPending { if toolCall.State.Status == opencode.ToolPartStateStatusPending {
return renderToolAction(toolCall.Tool) title := renderToolAction(toolCall.Tool)
return styles.NewStyle().Width(width - 6).Render(title)
} }
toolArgs := "" toolArgs := ""
@@ -596,7 +597,7 @@ func renderToolTitle(
func renderToolAction(name string) string { func renderToolAction(name string) string {
switch name { switch name {
case "task": case "task":
return "Searching..." return "Planning..."
case "bash": case "bash":
return "Writing command..." return "Writing command..."
case "edit": case "edit":
@@ -190,7 +190,7 @@ func (m *messagesComponent) renderView(width int) {
m.app, m.app,
message.Info, message.Info,
part.Text, part.Text,
m.app.Info.User, m.app.Config.Username,
m.showToolDetails, m.showToolDetails,
m.partCount == m.selectedPart, m.partCount == m.selectedPart,
width, width,
@@ -9,93 +9,17 @@ import (
"github.com/charmbracelet/bubbles/v2/textarea" "github.com/charmbracelet/bubbles/v2/textarea"
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/lithammer/fuzzysearch/fuzzy" "github.com/lithammer/fuzzysearch/fuzzy"
"github.com/muesli/reflow/truncate" "github.com/muesli/reflow/truncate"
"github.com/sst/opencode/internal/completions"
"github.com/sst/opencode/internal/components/list" "github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util" "github.com/sst/opencode/internal/util"
) )
type CompletionItem struct {
Title string
Value string
ProviderID string
Raw any
backgroundColor *compat.AdaptiveColor
}
type CompletionItemI interface {
list.ListItem
GetValue() string
DisplayValue() string
GetProviderID() string
GetRaw() any
}
func (ci *CompletionItem) Render(selected bool, width int) string {
t := theme.CurrentTheme()
baseStyle := styles.NewStyle().Foreground(t.Text())
truncatedStr := truncate.String(string(ci.DisplayValue()), uint(width-4))
backgroundColor := t.BackgroundElement()
if ci.backgroundColor != nil {
backgroundColor = *ci.backgroundColor
}
itemStyle := baseStyle.
Background(backgroundColor).
Padding(0, 1)
if selected {
itemStyle = itemStyle.Foreground(t.Primary())
}
title := itemStyle.Render(truncatedStr)
return title
}
func (ci *CompletionItem) DisplayValue() string {
return ci.Title
}
func (ci *CompletionItem) GetValue() string {
return ci.Value
}
func (ci *CompletionItem) GetProviderID() string {
return ci.ProviderID
}
func (ci *CompletionItem) GetRaw() any {
return ci.Raw
}
type CompletionItemOption func(*CompletionItem)
func WithBackgroundColor(color compat.AdaptiveColor) CompletionItemOption {
return func(ci *CompletionItem) {
ci.backgroundColor = &color
}
}
func NewCompletionItem(completionItem CompletionItem, opts ...CompletionItemOption) CompletionItemI {
for _, opt := range opts {
opt(&completionItem)
}
return &completionItem
}
type CompletionProvider interface {
GetId() string
GetChildEntries(query string) ([]CompletionItemI, error)
GetEmptyMessage() string
}
type CompletionSelectedMsg struct { type CompletionSelectedMsg struct {
Item CompletionItemI Item completions.CompletionSuggestion
SearchString string SearchString string
} }
@@ -114,11 +38,11 @@ type CompletionDialog interface {
type completionDialogComponent struct { type completionDialogComponent struct {
query string query string
providers []CompletionProvider providers []completions.CompletionProvider
width int width int
height int height int
pseudoSearchTextArea textarea.Model pseudoSearchTextArea textarea.Model
list list.List[CompletionItemI] list list.List[completions.CompletionSuggestion]
trigger string trigger string
} }
@@ -132,7 +56,7 @@ var completionDialogKeys = completionDialogKeyMap{
key.WithKeys("tab", "enter", "right"), key.WithKeys("tab", "enter", "right"),
), ),
Cancel: key.NewBinding( Cancel: key.NewBinding(
key.WithKeys(" ", "esc", "backspace", "ctrl+c"), key.WithKeys(" ", "esc", "backspace", "ctrl+h", "ctrl+c"),
), ),
} }
@@ -142,7 +66,7 @@ func (c *completionDialogComponent) Init() tea.Cmd {
func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd { func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
allItems := make([]CompletionItemI, 0) allItems := make([]completions.CompletionSuggestion, 0)
// Collect results from all providers // Collect results from all providers
for _, provider := range c.providers { for _, provider := range c.providers {
@@ -162,10 +86,12 @@ func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
// If there's a query, use fuzzy ranking to sort results // If there's a query, use fuzzy ranking to sort results
if query != "" && len(allItems) > 0 { if query != "" && len(allItems) > 0 {
t := theme.CurrentTheme()
baseStyle := styles.NewStyle().Background(t.BackgroundElement())
// Create a slice of display values for fuzzy matching // Create a slice of display values for fuzzy matching
displayValues := make([]string, len(allItems)) displayValues := make([]string, len(allItems))
for i, item := range allItems { for i, item := range allItems {
displayValues[i] = item.DisplayValue() displayValues[i] = item.Display(baseStyle)
} }
// Get fuzzy matches with ranking // Get fuzzy matches with ranking
@@ -175,7 +101,7 @@ func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
sort.Sort(matches) sort.Sort(matches)
// Reorder items based on fuzzy ranking // Reorder items based on fuzzy ranking
rankedItems := make([]CompletionItemI, 0, len(matches)) rankedItems := make([]completions.CompletionSuggestion, 0, len(matches))
for _, match := range matches { for _, match := range matches {
rankedItems = append(rankedItems, allItems[match.OriginalIndex]) rankedItems = append(rankedItems, allItems[match.OriginalIndex])
} }
@@ -189,7 +115,7 @@ func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd var cmds []tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
case []CompletionItemI: case []completions.CompletionSuggestion:
c.list.SetItems(msg) c.list.SetItems(msg)
case tea.KeyMsg: case tea.KeyMsg:
if c.pseudoSearchTextArea.Focused() { if c.pseudoSearchTextArea.Focused() {
@@ -207,7 +133,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
u, cmd := c.list.Update(msg) u, cmd := c.list.Update(msg)
c.list = u.(list.List[CompletionItemI]) c.list = u.(list.List[completions.CompletionSuggestion])
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
} }
@@ -223,7 +149,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
width := lipgloss.Width(value) width := lipgloss.Width(value)
triggerWidth := lipgloss.Width(c.trigger) triggerWidth := lipgloss.Width(c.trigger)
// Only close on backspace when there are no characters left, unless we're back to just the trigger // Only close on backspace when there are no characters left, unless we're back to just the trigger
if msg.String() != "backspace" || (width <= triggerWidth && value != c.trigger) { if (msg.String() != "backspace" && msg.String() != "ctrl+h") || (width <= triggerWidth && value != c.trigger) {
return c, c.close() return c, c.close()
} }
} }
@@ -241,11 +167,11 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (c *completionDialogComponent) View() string { func (c *completionDialogComponent) View() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
baseStyle := styles.NewStyle().Foreground(t.Text())
c.list.SetMaxWidth(c.width) c.list.SetMaxWidth(c.width)
return baseStyle. return styles.NewStyle().
Padding(0, 0). Padding(0, 1).
Foreground(t.Text()).
Background(t.BackgroundElement()). Background(t.BackgroundElement()).
BorderStyle(lipgloss.ThickBorder()). BorderStyle(lipgloss.ThickBorder()).
BorderLeft(true). BorderLeft(true).
@@ -264,7 +190,7 @@ func (c *completionDialogComponent) IsEmpty() bool {
return c.list.IsEmpty() return c.list.IsEmpty()
} }
func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd { func (c *completionDialogComponent) complete(item completions.CompletionSuggestion) tea.Cmd {
value := c.pseudoSearchTextArea.Value() value := c.pseudoSearchTextArea.Value()
return tea.Batch( return tea.Batch(
util.CmdHandler(CompletionSelectedMsg{ util.CmdHandler(CompletionSelectedMsg{
@@ -283,7 +209,7 @@ func (c *completionDialogComponent) close() tea.Cmd {
func NewCompletionDialogComponent( func NewCompletionDialogComponent(
trigger string, trigger string,
providers ...CompletionProvider, providers ...completions.CompletionProvider,
) CompletionDialog { ) CompletionDialog {
ti := textarea.New() ti := textarea.New()
ti.SetValue(trigger) ti.SetValue(trigger)
@@ -294,11 +220,34 @@ func NewCompletionDialogComponent(
emptyMessage = providers[0].GetEmptyMessage() emptyMessage = providers[0].GetEmptyMessage()
} }
// Define render function for completion suggestions
renderFunc := func(item completions.CompletionSuggestion, selected bool, width int, baseStyle styles.Style) string {
t := theme.CurrentTheme()
style := baseStyle
if selected {
style = style.Background(t.BackgroundElement()).Foreground(t.Primary())
} else {
style = style.Background(t.BackgroundElement()).Foreground(t.Text())
}
// The item.Display string already has any inline colors from the provider
truncatedStr := truncate.String(item.Display(style), uint(width-4))
return style.Width(width - 4).Render(truncatedStr)
}
// Define selectable function - all completion suggestions are selectable
selectableFunc := func(item completions.CompletionSuggestion) bool {
return true
}
li := list.NewListComponent( li := list.NewListComponent(
[]CompletionItemI{}, list.WithItems([]completions.CompletionSuggestion{}),
7, list.WithMaxVisibleHeight[completions.CompletionSuggestion](7),
emptyMessage, list.WithFallbackMessage[completions.CompletionSuggestion](emptyMessage),
false, list.WithAlphaNumericKeys[completions.CompletionSuggestion](false),
list.WithRenderFunc(renderFunc),
list.WithSelectableFunc(selectableFunc),
) )
c := &completionDialogComponent{ c := &completionDialogComponent{
@@ -311,7 +260,7 @@ func NewCompletionDialogComponent(
// Load initial items from all providers // Load initial items from all providers
go func() { go func() {
allItems := make([]CompletionItemI, 0) allItems := make([]completions.CompletionSuggestion, 0)
for _, provider := range providers { for _, provider := range providers {
items, err := provider.GetChildEntries("") items, err := provider.GetChildEntries("")
if err != nil { if err != nil {
+145 -146
View File
@@ -3,9 +3,8 @@ package dialog
import ( import (
"log/slog" "log/slog"
"github.com/charmbracelet/bubbles/v2/key"
"github.com/charmbracelet/bubbles/v2/textinput"
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/sst/opencode/internal/completions"
"github.com/sst/opencode/internal/components/list" "github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/components/modal" "github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/layout"
@@ -14,12 +13,20 @@ import (
"github.com/sst/opencode/internal/util" "github.com/sst/opencode/internal/util"
) )
const (
findDialogWidth = 76
)
type FindSelectedMsg struct { type FindSelectedMsg struct {
FilePath string FilePath string
} }
type FindDialogCloseMsg struct{} type FindDialogCloseMsg struct{}
type findInitialSuggestionsMsg struct {
suggestions []completions.CompletionSuggestion
}
type FindDialog interface { type FindDialog interface {
layout.Modal layout.Modal
tea.Model tea.Model
@@ -29,121 +36,157 @@ type FindDialog interface {
IsEmpty() bool IsEmpty() bool
} }
// findItem is a custom list item for file suggestions
type findItem struct {
suggestion completions.CompletionSuggestion
}
func (f findItem) Render(
selected bool,
width int,
baseStyle styles.Style,
) string {
t := theme.CurrentTheme()
itemStyle := baseStyle.
Background(t.BackgroundPanel()).
Foreground(t.TextMuted())
if selected {
itemStyle = itemStyle.Foreground(t.Primary())
}
return itemStyle.PaddingLeft(1).Render(f.suggestion.Display(itemStyle))
}
func (f findItem) Selectable() bool {
return true
}
type findDialogComponent struct { type findDialogComponent struct {
query string completionProvider completions.CompletionProvider
completionProvider CompletionProvider allSuggestions []completions.CompletionSuggestion
width, height int width, height int
modal *modal.Modal modal *modal.Modal
textInput textinput.Model searchDialog *SearchDialog
list list.List[CompletionItemI] dialogWidth int
}
type findDialogKeyMap struct {
Select key.Binding
Cancel key.Binding
}
var findDialogKeys = findDialogKeyMap{
Select: key.NewBinding(
key.WithKeys("enter"),
),
Cancel: key.NewBinding(
key.WithKeys("esc"),
),
} }
func (f *findDialogComponent) Init() tea.Cmd { func (f *findDialogComponent) Init() tea.Cmd {
return textinput.Blink return tea.Batch(
f.loadInitialSuggestions(),
f.searchDialog.Init(),
)
}
func (f *findDialogComponent) loadInitialSuggestions() tea.Cmd {
return func() tea.Msg {
items, err := f.completionProvider.GetChildEntries("")
if err != nil {
slog.Error("Failed to get initial completion items", "error", err)
return findInitialSuggestionsMsg{suggestions: []completions.CompletionSuggestion{}}
}
return findInitialSuggestionsMsg{suggestions: items}
}
} }
func (f *findDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (f *findDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
var cmds []tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
case []CompletionItemI: case findInitialSuggestionsMsg:
f.list.SetItems(msg) // Handle initial suggestions setup
case tea.KeyMsg: f.allSuggestions = msg.suggestions
switch msg.String() {
case "ctrl+c": // Calculate dialog width
if f.textInput.Value() == "" { f.dialogWidth = f.calculateDialogWidth()
return f, nil
} // Initialize search dialog with calculated width
f.textInput.SetValue("") f.searchDialog = NewSearchDialog("Search files...", 10)
return f.update(msg) f.searchDialog.SetWidth(f.dialogWidth)
// Convert to list items
items := make([]list.Item, len(f.allSuggestions))
for i, suggestion := range f.allSuggestions {
items[i] = findItem{suggestion: suggestion}
} }
f.searchDialog.SetItems(items)
switch { // Update modal with calculated width
case key.Matches(msg, findDialogKeys.Select): f.modal = modal.New(
item, i := f.list.GetSelectedItem() modal.WithTitle("Find Files"),
if i == -1 { modal.WithMaxWidth(f.dialogWidth+4),
return f, nil )
}
return f, f.selectFile(item)
case key.Matches(msg, findDialogKeys.Cancel):
return f, f.Close()
default:
f.textInput, cmd = f.textInput.Update(msg)
cmds = append(cmds, cmd)
f, cmd = f.update(msg) return f, f.searchDialog.Init()
cmds = append(cmds, cmd)
case []completions.CompletionSuggestion:
// Store suggestions and convert to findItem for the search dialog
f.allSuggestions = msg
items := make([]list.Item, len(msg))
for i, suggestion := range msg {
items[i] = findItem{suggestion: suggestion}
} }
} f.searchDialog.SetItems(items)
return f, nil
return f, tea.Batch(cmds...) case SearchSelectionMsg:
} // Handle selection from search dialog - now we can directly access the suggestion
if item, ok := msg.Item.(findItem); ok {
return f, f.selectFile(item.suggestion)
}
return f, nil
func (f *findDialogComponent) update(msg tea.Msg) (*findDialogComponent, tea.Cmd) { case SearchCancelledMsg:
var cmd tea.Cmd return f, f.Close()
var cmds []tea.Cmd
query := f.textInput.Value() case SearchQueryChangedMsg:
if query != f.query { // Update completion items based on search query
f.query = query return f, func() tea.Msg {
cmd = func() tea.Msg { items, err := f.completionProvider.GetChildEntries(msg.Query)
items, err := f.completionProvider.GetChildEntries(query)
if err != nil { if err != nil {
slog.Error("Failed to get completion items", "error", err) slog.Error("Failed to get completion items", "error", err)
return []completions.CompletionSuggestion{}
} }
return items return items
} }
cmds = append(cmds, cmd)
case tea.WindowSizeMsg:
f.width = msg.Width
f.height = msg.Height
// Recalculate width based on new viewport size
oldWidth := f.dialogWidth
f.dialogWidth = f.calculateDialogWidth()
if oldWidth != f.dialogWidth {
f.searchDialog.SetWidth(f.dialogWidth)
// Update modal max width too
f.modal = modal.New(
modal.WithTitle("Find Files"),
modal.WithMaxWidth(f.dialogWidth+4),
)
}
f.searchDialog.SetHeight(msg.Height)
} }
u, cmd := f.list.Update(msg) // Forward all other messages to the search dialog
f.list = u.(list.List[CompletionItemI]) updatedDialog, cmd := f.searchDialog.Update(msg)
cmds = append(cmds, cmd) f.searchDialog = updatedDialog.(*SearchDialog)
return f, cmd
return f, tea.Batch(cmds...)
} }
func (f *findDialogComponent) View() string { func (f *findDialogComponent) View() string {
t := theme.CurrentTheme() return f.searchDialog.View()
f.textInput.SetWidth(f.width - 8) }
f.list.SetMaxWidth(f.width - 4)
inputView := f.textInput.View()
inputView = styles.NewStyle().
Background(t.BackgroundElement()).
Height(1).
Width(f.width-4).
Padding(0, 0).
Render(inputView)
listView := f.list.View() func (f *findDialogComponent) calculateDialogWidth() int {
return styles.NewStyle(). // Use fixed width unless viewport is smaller
Height(12). if f.width > 0 && f.width < findDialogWidth+10 {
Background(t.BackgroundPanel()). return f.width - 10
Width(f.width - 4). }
Render(inputView + "\n" + listView) return findDialogWidth
} }
func (f *findDialogComponent) SetWidth(width int) { func (f *findDialogComponent) SetWidth(width int) {
f.width = width f.width = width
if width > 4 { f.searchDialog.SetWidth(f.dialogWidth)
f.textInput.SetWidth(width - 4)
f.list.SetMaxWidth(width - 4)
}
} }
func (f *findDialogComponent) SetHeight(height int) { func (f *findDialogComponent) SetHeight(height int) {
@@ -151,14 +194,14 @@ func (f *findDialogComponent) SetHeight(height int) {
} }
func (f *findDialogComponent) IsEmpty() bool { func (f *findDialogComponent) IsEmpty() bool {
return f.list.IsEmpty() return f.searchDialog.GetQuery() == ""
} }
func (f *findDialogComponent) selectFile(item CompletionItemI) tea.Cmd { func (f *findDialogComponent) selectFile(item completions.CompletionSuggestion) tea.Cmd {
return tea.Sequence( return tea.Sequence(
f.Close(), f.Close(),
util.CmdHandler(FindSelectedMsg{ util.CmdHandler(FindSelectedMsg{
FilePath: item.GetValue(), FilePath: item.Value,
}), }),
) )
} }
@@ -168,70 +211,26 @@ func (f *findDialogComponent) Render(background string) string {
} }
func (f *findDialogComponent) Close() tea.Cmd { func (f *findDialogComponent) Close() tea.Cmd {
f.textInput.Reset() f.searchDialog.SetQuery("")
f.textInput.Blur() f.searchDialog.Blur()
return util.CmdHandler(modal.CloseModalMsg{}) return util.CmdHandler(modal.CloseModalMsg{})
} }
func createTextInput(existing *textinput.Model) textinput.Model { func NewFindDialog(completionProvider completions.CompletionProvider) FindDialog {
t := theme.CurrentTheme() component := &findDialogComponent{
bgColor := t.BackgroundElement() completionProvider: completionProvider,
textColor := t.Text() dialogWidth: findDialogWidth,
textMutedColor := t.TextMuted() allSuggestions: []completions.CompletionSuggestion{},
ti := textinput.New()
ti.Styles.Blurred.Placeholder = styles.NewStyle().
Foreground(textMutedColor).
Background(bgColor).
Lipgloss()
ti.Styles.Blurred.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
ti.Styles.Focused.Placeholder = styles.NewStyle().
Foreground(textMutedColor).
Background(bgColor).
Lipgloss()
ti.Styles.Focused.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
ti.Styles.Cursor.Color = t.Primary()
ti.VirtualCursor = true
ti.Prompt = " "
ti.CharLimit = -1
ti.Focus()
if existing != nil {
ti.SetValue(existing.Value())
ti.SetWidth(existing.Width())
} }
return ti // Create search dialog and modal with fixed width
} component.searchDialog = NewSearchDialog("Search files...", 10)
component.searchDialog.SetWidth(findDialogWidth)
func NewFindDialog(completionProvider CompletionProvider) FindDialog { component.modal = modal.New(
ti := createTextInput(nil) modal.WithTitle("Find Files"),
modal.WithMaxWidth(findDialogWidth+4),
li := list.NewListComponent(
[]CompletionItemI{},
10, // max visible items
completionProvider.GetEmptyMessage(),
false,
) )
go func() { return component
items, err := completionProvider.GetChildEntries("")
if err != nil {
slog.Error("Failed to get completion items", "error", err)
}
li.SetItems(items)
}()
return &findDialogComponent{
query: "",
completionProvider: completionProvider,
textInput: ti,
list: li,
modal: modal.New(
modal.WithTitle("Find Files"),
modal.WithMaxWidth(80),
),
}
} }
+258 -74
View File
@@ -8,6 +8,7 @@ import (
"github.com/charmbracelet/bubbles/v2/key" "github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/sst/opencode-sdk-go" "github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/list" "github.com/sst/opencode/internal/components/list"
@@ -22,6 +23,7 @@ const (
numVisibleModels = 10 numVisibleModels = 10
minDialogWidth = 40 minDialogWidth = 40
maxDialogWidth = 80 maxDialogWidth = 80
maxRecentModels = 5
) )
// ModelDialog interface for the model selection dialog // ModelDialog interface for the model selection dialog
@@ -30,13 +32,13 @@ type ModelDialog interface {
} }
type modelDialog struct { type modelDialog struct {
app *app.App app *app.App
allModels []ModelWithProvider allModels []ModelWithProvider
width int width int
height int height int
modal *modal.Modal modal *modal.Modal
modelList list.List[ModelItem] searchDialog *SearchDialog
dialogWidth int dialogWidth int
} }
type ModelWithProvider struct { type ModelWithProvider struct {
@@ -44,39 +46,42 @@ type ModelWithProvider struct {
Provider opencode.Provider Provider opencode.Provider
} }
type ModelItem struct { // modelItem is a custom list item for model selections
ModelName string type modelItem struct {
ProviderName string model ModelWithProvider
} }
func (m ModelItem) Render(selected bool, width int) string { func (m modelItem) Render(
selected bool,
width int,
baseStyle styles.Style,
) string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
itemStyle := baseStyle.
Background(t.BackgroundPanel()).
Foreground(t.Text())
if selected { if selected {
displayText := fmt.Sprintf("%s (%s)", m.ModelName, m.ProviderName) itemStyle = itemStyle.Foreground(t.Primary())
return styles.NewStyle().
Background(t.Primary()).
Foreground(t.BackgroundPanel()).
Width(width).
PaddingLeft(1).
Render(displayText)
} else {
modelStyle := styles.NewStyle().
Foreground(t.Text()).
Background(t.BackgroundPanel())
providerStyle := styles.NewStyle().
Foreground(t.TextMuted()).
Background(t.BackgroundPanel())
modelPart := modelStyle.Render(m.ModelName)
providerPart := providerStyle.Render(fmt.Sprintf(" (%s)", m.ProviderName))
combinedText := modelPart + providerPart
return styles.NewStyle().
Background(t.BackgroundPanel()).
PaddingLeft(1).
Render(combinedText)
} }
providerStyle := baseStyle.
Foreground(t.TextMuted()).
Background(t.BackgroundPanel())
modelPart := itemStyle.Render(m.model.Model.Name)
providerPart := providerStyle.Render(fmt.Sprintf(" %s", m.model.Provider.Name))
combinedText := modelPart + providerPart
return baseStyle.
Background(t.BackgroundPanel()).
PaddingLeft(1).
Render(combinedText)
}
func (m modelItem) Selectable() bool {
return true
} }
type modelKeyMap struct { type modelKeyMap struct {
@@ -97,52 +102,67 @@ var modelKeys = modelKeyMap{
func (m *modelDialog) Init() tea.Cmd { func (m *modelDialog) Init() tea.Cmd {
m.setupAllModels() m.setupAllModels()
return nil return m.searchDialog.Init()
} }
func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case SearchSelectionMsg:
switch { // Handle selection from search dialog
case key.Matches(msg, modelKeys.Enter): if item, ok := msg.Item.(modelItem); ok {
_, selectedIndex := m.modelList.GetSelectedItem() return m, tea.Sequence(
if selectedIndex >= 0 && selectedIndex < len(m.allModels) { util.CmdHandler(modal.CloseModalMsg{}),
selectedModel := m.allModels[selectedIndex] util.CmdHandler(
return m, tea.Sequence( app.ModelSelectedMsg{
util.CmdHandler(modal.CloseModalMsg{}), Provider: item.model.Provider,
util.CmdHandler( Model: item.model.Model,
app.ModelSelectedMsg{ }),
Provider: selectedModel.Provider, )
Model: selectedModel.Model,
}),
)
}
return m, util.CmdHandler(modal.CloseModalMsg{})
case key.Matches(msg, modelKeys.Escape):
return m, util.CmdHandler(modal.CloseModalMsg{})
} }
return m, util.CmdHandler(modal.CloseModalMsg{})
case SearchCancelledMsg:
return m, util.CmdHandler(modal.CloseModalMsg{})
case SearchRemoveItemMsg:
if item, ok := msg.Item.(modelItem); ok {
if m.isModelInRecentSection(item.model, msg.Index) {
m.app.State.RemoveModelFromRecentlyUsed(item.model.Provider.ID, item.model.Model.ID)
m.app.SaveState()
items := m.buildDisplayList(m.searchDialog.GetQuery())
m.searchDialog.SetItems(items)
}
}
return m, nil
case SearchQueryChangedMsg:
// Update the list based on search query
items := m.buildDisplayList(msg.Query)
m.searchDialog.SetItems(items)
return m, nil
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.width = msg.Width m.width = msg.Width
m.height = msg.Height m.height = msg.Height
m.searchDialog.SetWidth(m.dialogWidth)
m.searchDialog.SetHeight(msg.Height)
} }
// Update the list component updatedDialog, cmd := m.searchDialog.Update(msg)
updatedList, cmd := m.modelList.Update(msg) m.searchDialog = updatedDialog.(*SearchDialog)
m.modelList = updatedList.(list.List[ModelItem])
return m, cmd return m, cmd
} }
func (m *modelDialog) View() string { func (m *modelDialog) View() string {
return m.modelList.View() return m.searchDialog.View()
} }
func (m *modelDialog) calculateOptimalWidth(modelItems []ModelItem) int { func (m *modelDialog) calculateOptimalWidth(models []ModelWithProvider) int {
maxWidth := minDialogWidth maxWidth := minDialogWidth
for _, item := range modelItems { for _, model := range models {
// Calculate the width needed for this item: "ModelName (ProviderName)" // Calculate the width needed for this item: "ModelName (ProviderName)"
// Add 4 for the parentheses, space, and some padding // Add 4 for the parentheses, space, and some padding
itemWidth := len(item.ModelName) + len(item.ProviderName) + 4 itemWidth := len(model.Model.Name) + len(model.Provider.Name) + 4
if itemWidth > maxWidth { if itemWidth > maxWidth {
maxWidth = itemWidth maxWidth = itemWidth
} }
@@ -170,22 +190,16 @@ func (m *modelDialog) setupAllModels() {
m.sortModels() m.sortModels()
modelItems := make([]ModelItem, len(m.allModels)) // Calculate optimal width based on all models
for i, modelWithProvider := range m.allModels { m.dialogWidth = m.calculateOptimalWidth(m.allModels)
modelItems[i] = ModelItem{
ModelName: modelWithProvider.Model.Name,
ProviderName: modelWithProvider.Provider.Name,
}
}
m.dialogWidth = m.calculateOptimalWidth(modelItems) // Initialize search dialog
m.searchDialog = NewSearchDialog("Search models...", numVisibleModels)
m.searchDialog.SetWidth(m.dialogWidth)
m.modelList = list.NewListComponent(modelItems, numVisibleModels, "No models available", true) // Build initial display list (empty query shows grouped view)
m.modelList.SetMaxWidth(m.dialogWidth) items := m.buildDisplayList("")
m.searchDialog.SetItems(items)
if len(m.allModels) > 0 {
m.modelList.SetSelectedIndex(0)
}
} }
func (m *modelDialog) sortModels() { func (m *modelDialog) sortModels() {
@@ -248,6 +262,176 @@ func (m *modelDialog) getModelUsageTime(providerID, modelID string) time.Time {
return time.Time{} return time.Time{}
} }
// buildDisplayList creates the list items based on search query
func (m *modelDialog) buildDisplayList(query string) []list.Item {
if query != "" {
// Search mode: use fuzzy matching
return m.buildSearchResults(query)
} else {
// Grouped mode: show Recent section and provider groups
return m.buildGroupedResults()
}
}
// buildSearchResults creates a flat list of search results using fuzzy matching
func (m *modelDialog) buildSearchResults(query string) []list.Item {
type modelMatch struct {
model ModelWithProvider
score int
}
modelNames := []string{}
modelMap := make(map[string]ModelWithProvider)
// Create search strings and perform fuzzy matching
for _, model := range m.allModels {
searchStr := fmt.Sprintf("%s %s", model.Model.Name, model.Provider.Name)
modelNames = append(modelNames, searchStr)
modelMap[searchStr] = model
searchStr = fmt.Sprintf("%s %s", model.Provider.Name, model.Model.Name)
modelNames = append(modelNames, searchStr)
modelMap[searchStr] = model
}
matches := fuzzy.RankFindFold(query, modelNames)
sort.Sort(matches)
items := []list.Item{}
seenModels := make(map[string]bool)
for _, match := range matches {
model := modelMap[match.Target]
// Create a unique key to avoid duplicates
key := fmt.Sprintf("%s:%s", model.Provider.ID, model.Model.ID)
if seenModels[key] {
continue
}
seenModels[key] = true
items = append(items, modelItem{model: model})
}
return items
}
// buildGroupedResults creates a grouped list with Recent section and provider groups
func (m *modelDialog) buildGroupedResults() []list.Item {
var items []list.Item
// Add Recent section
recentModels := m.getRecentModels(maxRecentModels)
if len(recentModels) > 0 {
items = append(items, list.HeaderItem("Recent"))
for _, model := range recentModels {
items = append(items, modelItem{model: model})
}
}
// Group models by provider
providerGroups := make(map[string][]ModelWithProvider)
for _, model := range m.allModels {
providerName := model.Provider.Name
providerGroups[providerName] = append(providerGroups[providerName], model)
}
// Get sorted provider names for consistent order
var providerNames []string
for name := range providerGroups {
providerNames = append(providerNames, name)
}
sort.Strings(providerNames)
// Add provider groups
for _, providerName := range providerNames {
models := providerGroups[providerName]
// Sort models within provider group
sort.Slice(models, func(i, j int) bool {
modelA := models[i]
modelB := models[j]
usageA := m.getModelUsageTime(modelA.Provider.ID, modelA.Model.ID)
usageB := m.getModelUsageTime(modelB.Provider.ID, modelB.Model.ID)
// Sort by usage time first, then by release date, then alphabetically
if !usageA.IsZero() && !usageB.IsZero() {
return usageA.After(usageB)
}
if !usageA.IsZero() && usageB.IsZero() {
return true
}
if usageA.IsZero() && !usageB.IsZero() {
return false
}
// Sort by release date if available
if modelA.Model.ReleaseDate != "" && modelB.Model.ReleaseDate != "" {
dateA := m.parseReleaseDate(modelA.Model.ReleaseDate)
dateB := m.parseReleaseDate(modelB.Model.ReleaseDate)
if !dateA.IsZero() && !dateB.IsZero() {
return dateA.After(dateB)
}
}
return modelA.Model.Name < modelB.Model.Name
})
// Add provider header
items = append(items, list.HeaderItem(providerName))
// Add models in this provider group
for _, model := range models {
items = append(items, modelItem{model: model})
}
}
return items
}
// getRecentModels returns the most recently used models
func (m *modelDialog) getRecentModels(limit int) []ModelWithProvider {
var recentModels []ModelWithProvider
// Get recent models from app state
for _, usage := range m.app.State.RecentlyUsedModels {
if len(recentModels) >= limit {
break
}
// Find the corresponding model
for _, model := range m.allModels {
if model.Provider.ID == usage.ProviderID && model.Model.ID == usage.ModelID {
recentModels = append(recentModels, model)
break
}
}
}
return recentModels
}
func (m *modelDialog) isModelInRecentSection(model ModelWithProvider, index int) bool {
// Only check if we're in grouped mode (no search query)
if m.searchDialog.GetQuery() != "" {
return false
}
recentModels := m.getRecentModels(maxRecentModels)
if len(recentModels) == 0 {
return false
}
// Index 0 is the "Recent" header, so recent models are at indices 1 to len(recentModels)
if index >= 1 && index <= len(recentModels) {
if index-1 < len(recentModels) {
recentModel := recentModels[index-1]
return recentModel.Provider.ID == model.Provider.ID && recentModel.Model.ID == model.Model.ID
}
}
return false
}
func (m *modelDialog) Render(background string) string { func (m *modelDialog) Render(background string) string {
return m.modal.Render(m.View(), background) return m.modal.Render(m.View(), background)
} }
@@ -1,496 +0,0 @@
package dialog
import (
"fmt"
"github.com/charmbracelet/bubbles/v2/key"
"github.com/charmbracelet/bubbles/v2/viewport"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
"strings"
)
type PermissionAction string
// Permission responses
const (
PermissionAllow PermissionAction = "allow"
PermissionAllowForSession PermissionAction = "allow_session"
PermissionDeny PermissionAction = "deny"
)
// PermissionResponseMsg represents the user's response to a permission request
type PermissionResponseMsg struct {
// Permission permission.PermissionRequest
Action PermissionAction
}
// PermissionDialogComponent interface for permission dialog component
type PermissionDialogComponent interface {
tea.Model
tea.ViewModel
// SetPermissions(permission permission.PermissionRequest) tea.Cmd
}
type permissionsMapping struct {
Left key.Binding
Right key.Binding
EnterSpace key.Binding
Allow key.Binding
AllowSession key.Binding
Deny key.Binding
Tab key.Binding
}
var permissionsKeys = permissionsMapping{
Left: key.NewBinding(
key.WithKeys("left"),
key.WithHelp("←", "switch options"),
),
Right: key.NewBinding(
key.WithKeys("right"),
key.WithHelp("→", "switch options"),
),
EnterSpace: key.NewBinding(
key.WithKeys("enter", " "),
key.WithHelp("enter/space", "confirm"),
),
Allow: key.NewBinding(
key.WithKeys("a"),
key.WithHelp("a", "allow"),
),
AllowSession: key.NewBinding(
key.WithKeys("s"),
key.WithHelp("s", "allow for session"),
),
Deny: key.NewBinding(
key.WithKeys("d"),
key.WithHelp("d", "deny"),
),
Tab: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "switch options"),
),
}
// permissionDialogComponent is the implementation of PermissionDialog
type permissionDialogComponent struct {
width int
height int
// permission permission.PermissionRequest
windowSize tea.WindowSizeMsg
contentViewPort viewport.Model
selectedOption int // 0: Allow, 1: Allow for session, 2: Deny
diffCache map[string]string
markdownCache map[string]string
}
func (p *permissionDialogComponent) Init() tea.Cmd {
return p.contentViewPort.Init()
}
func (p *permissionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
p.windowSize = msg
cmd := p.SetSize()
cmds = append(cmds, cmd)
p.markdownCache = make(map[string]string)
p.diffCache = make(map[string]string)
// case tea.KeyMsg:
// switch {
// case key.Matches(msg, permissionsKeys.Right) || key.Matches(msg, permissionsKeys.Tab):
// p.selectedOption = (p.selectedOption + 1) % 3
// return p, nil
// case key.Matches(msg, permissionsKeys.Left):
// p.selectedOption = (p.selectedOption + 2) % 3
// case key.Matches(msg, permissionsKeys.EnterSpace):
// return p, p.selectCurrentOption()
// case key.Matches(msg, permissionsKeys.Allow):
// return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllow, Permission: p.permission})
// case key.Matches(msg, permissionsKeys.AllowSession):
// return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllowForSession, Permission: p.permission})
// case key.Matches(msg, permissionsKeys.Deny):
// return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionDeny, Permission: p.permission})
// default:
// // Pass other keys to viewport
// viewPort, cmd := p.contentViewPort.Update(msg)
// p.contentViewPort = viewPort
// cmds = append(cmds, cmd)
// }
}
return p, tea.Batch(cmds...)
}
func (p *permissionDialogComponent) selectCurrentOption() tea.Cmd {
var action PermissionAction
switch p.selectedOption {
case 0:
action = PermissionAllow
case 1:
action = PermissionAllowForSession
case 2:
action = PermissionDeny
}
return util.CmdHandler(PermissionResponseMsg{Action: action}) // , Permission: p.permission})
}
func (p *permissionDialogComponent) renderButtons() string {
t := theme.CurrentTheme()
baseStyle := styles.NewStyle().Foreground(t.Text())
allowStyle := baseStyle
allowSessionStyle := baseStyle
denyStyle := baseStyle
spacerStyle := baseStyle.Background(t.Background())
// Style the selected button
switch p.selectedOption {
case 0:
allowStyle = allowStyle.Background(t.Primary()).Foreground(t.Background())
allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
case 1:
allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background())
denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
case 2:
allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background())
}
allowButton := allowStyle.Padding(0, 1).Render("Allow (a)")
allowSessionButton := allowSessionStyle.Padding(0, 1).Render("Allow for session (s)")
denyButton := denyStyle.Padding(0, 1).Render("Deny (d)")
content := lipgloss.JoinHorizontal(
lipgloss.Left,
allowButton,
spacerStyle.Render(" "),
allowSessionButton,
spacerStyle.Render(" "),
denyButton,
spacerStyle.Render(" "),
)
remainingWidth := p.width - lipgloss.Width(content)
if remainingWidth > 0 {
content = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + content
}
return content
}
func (p *permissionDialogComponent) renderHeader() string {
return "NOT IMPLEMENTED"
// t := theme.CurrentTheme()
// baseStyle := styles.BaseStyle()
//
// toolKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Tool")
// toolValue := baseStyle.
// Foreground(t.Text()).
// Width(p.width - lipgloss.Width(toolKey)).
// Render(fmt.Sprintf(": %s", p.permission.ToolName))
//
// pathKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Path")
//
// // Get the current working directory to display relative path
// relativePath := p.permission.Path
// if filepath.IsAbs(relativePath) {
// if cwd, err := filepath.Rel(config.WorkingDirectory(), relativePath); err == nil {
// relativePath = cwd
// }
// }
//
// pathValue := baseStyle.
// Foreground(t.Text()).
// Width(p.width - lipgloss.Width(pathKey)).
// Render(fmt.Sprintf(": %s", relativePath))
//
// headerParts := []string{
// lipgloss.JoinHorizontal(
// lipgloss.Left,
// toolKey,
// toolValue,
// ),
// baseStyle.Render(strings.Repeat(" ", p.width)),
// lipgloss.JoinHorizontal(
// lipgloss.Left,
// pathKey,
// pathValue,
// ),
// baseStyle.Render(strings.Repeat(" ", p.width)),
// }
//
// // Add tool-specific header information
// switch p.permission.ToolName {
// case "bash":
// headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Command"))
// case "edit":
// headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
// case "write":
// headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
// case "fetch":
// headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("URL"))
// }
//
// return lipgloss.NewStyle().Background(t.Background()).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
}
func (p *permissionDialogComponent) renderBashContent() string {
// t := theme.CurrentTheme()
// baseStyle := styles.BaseStyle()
//
// if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
// content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
//
// // Use the cache for markdown rendering
// renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
// r := styles.GetMarkdownRenderer(p.width - 10)
// s, err := r.Render(content)
// return s
// })
//
// finalContent := baseStyle.
// Width(p.contentViewPort.Width).
// Render(renderedContent)
// p.contentViewPort.SetContent(finalContent)
// return p.styleViewport()
// }
return ""
}
func (p *permissionDialogComponent) renderEditContent() string {
// if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
// diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
// return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
// })
//
// p.contentViewPort.SetContent(diff)
// return p.styleViewport()
// }
return ""
}
func (p *permissionDialogComponent) renderPatchContent() string {
// if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
// diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
// return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
// })
//
// p.contentViewPort.SetContent(diff)
// return p.styleViewport()
// }
return ""
}
func (p *permissionDialogComponent) renderWriteContent() string {
// if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok {
// // Use the cache for diff rendering
// diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
// return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
// })
//
// p.contentViewPort.SetContent(diff)
// return p.styleViewport()
// }
return ""
}
func (p *permissionDialogComponent) renderFetchContent() string {
// t := theme.CurrentTheme()
// baseStyle := styles.BaseStyle()
//
// if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
// content := fmt.Sprintf("```bash\n%s\n```", pr.URL)
//
// // Use the cache for markdown rendering
// renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
// r := styles.GetMarkdownRenderer(p.width - 10)
// s, err := r.Render(content)
// return s
// })
//
// finalContent := baseStyle.
// Width(p.contentViewPort.Width).
// Render(renderedContent)
// p.contentViewPort.SetContent(finalContent)
// return p.styleViewport()
// }
return ""
}
func (p *permissionDialogComponent) renderDefaultContent() string {
// t := theme.CurrentTheme()
// baseStyle := styles.BaseStyle()
//
// content := p.permission.Description
//
// // Use the cache for markdown rendering
// renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
// r := styles.GetMarkdownRenderer(p.width - 10)
// s, err := r.Render(content)
// return s
// })
//
// finalContent := baseStyle.
// Width(p.contentViewPort.Width).
// Render(renderedContent)
// p.contentViewPort.SetContent(finalContent)
//
// if renderedContent == "" {
// return ""
// }
//
return p.styleViewport()
}
func (p *permissionDialogComponent) styleViewport() string {
t := theme.CurrentTheme()
contentStyle := styles.NewStyle().Background(t.Background())
return contentStyle.Render(p.contentViewPort.View())
}
func (p *permissionDialogComponent) render() string {
return "NOT IMPLEMENTED"
// t := theme.CurrentTheme()
// baseStyle := styles.BaseStyle()
//
// title := baseStyle.
// Bold(true).
// Width(p.width - 4).
// Foreground(t.Primary()).
// Render("Permission Required")
// // Render header
// headerContent := p.renderHeader()
// // Render buttons
// buttons := p.renderButtons()
//
// // Calculate content height dynamically based on window size
// p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(buttons) - 2 - lipgloss.Height(title)
// p.contentViewPort.Width = p.width - 4
//
// // Render content based on tool type
// var contentFinal string
// switch p.permission.ToolName {
// case "bash":
// contentFinal = p.renderBashContent()
// case "edit":
// contentFinal = p.renderEditContent()
// case "patch":
// contentFinal = p.renderPatchContent()
// case "write":
// contentFinal = p.renderWriteContent()
// case "fetch":
// contentFinal = p.renderFetchContent()
// default:
// contentFinal = p.renderDefaultContent()
// }
//
// content := lipgloss.JoinVertical(
// lipgloss.Top,
// title,
// baseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
// headerContent,
// contentFinal,
// buttons,
// baseStyle.Render(strings.Repeat(" ", p.width-4)),
// )
//
// return baseStyle.
// Padding(1, 0, 0, 1).
// Border(lipgloss.RoundedBorder()).
// BorderBackground(t.Background()).
// BorderForeground(t.TextMuted()).
// Width(p.width).
// Height(p.height).
// Render(
// content,
// )
}
func (p *permissionDialogComponent) View() string {
return p.render()
}
func (p *permissionDialogComponent) SetSize() tea.Cmd {
// if p.permission.ID == "" {
// return nil
// }
// switch p.permission.ToolName {
// case "bash":
// p.width = int(float64(p.windowSize.Width) * 0.4)
// p.height = int(float64(p.windowSize.Height) * 0.3)
// case "edit":
// p.width = int(float64(p.windowSize.Width) * 0.8)
// p.height = int(float64(p.windowSize.Height) * 0.8)
// case "write":
// p.width = int(float64(p.windowSize.Width) * 0.8)
// p.height = int(float64(p.windowSize.Height) * 0.8)
// case "fetch":
// p.width = int(float64(p.windowSize.Width) * 0.4)
// p.height = int(float64(p.windowSize.Height) * 0.3)
// default:
// p.width = int(float64(p.windowSize.Width) * 0.7)
// p.height = int(float64(p.windowSize.Height) * 0.5)
// }
return nil
}
// func (p *permissionDialogCmp) SetPermissions(permission permission.PermissionRequest) tea.Cmd {
// p.permission = permission
// return p.SetSize()
// }
// Helper to get or set cached diff content
func (c *permissionDialogComponent) GetOrSetDiff(key string, generator func() (string, error)) string {
if cached, ok := c.diffCache[key]; ok {
return cached
}
content, err := generator()
if err != nil {
return fmt.Sprintf("Error formatting diff: %v", err)
}
c.diffCache[key] = content
return content
}
// Helper to get or set cached markdown content
func (c *permissionDialogComponent) GetOrSetMarkdown(key string, generator func() (string, error)) string {
if cached, ok := c.markdownCache[key]; ok {
return cached
}
content, err := generator()
if err != nil {
return fmt.Sprintf("Error rendering markdown: %v", err)
}
c.markdownCache[key] = content
return content
}
func NewPermissionDialogCmp() PermissionDialogComponent {
// Create viewport for content
contentViewport := viewport.New() // (0, 0)
return &permissionDialogComponent{
contentViewPort: contentViewport,
selectedOption: 0, // Default to "Allow"
diffCache: make(map[string]string),
markdownCache: make(map[string]string),
}
}
@@ -0,0 +1,247 @@
package dialog
import (
"github.com/charmbracelet/bubbles/v2/key"
"github.com/charmbracelet/bubbles/v2/textinput"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
// SearchQueryChangedMsg is emitted when the search query changes
type SearchQueryChangedMsg struct {
Query string
}
// SearchSelectionMsg is emitted when an item is selected
type SearchSelectionMsg struct {
Item any
Index int
}
// SearchCancelledMsg is emitted when the search is cancelled
type SearchCancelledMsg struct{}
// SearchRemoveItemMsg is emitted when Ctrl+X is pressed to remove an item
type SearchRemoveItemMsg struct {
Item any
Index int
}
// SearchDialog is a reusable component that combines a text input with a list
type SearchDialog struct {
textInput textinput.Model
list list.List[list.Item]
width int
height int
focused bool
}
type searchKeyMap struct {
Up key.Binding
Down key.Binding
Enter key.Binding
Escape key.Binding
Remove key.Binding
}
var searchKeys = searchKeyMap{
Up: key.NewBinding(
key.WithKeys("up", "ctrl+p"),
key.WithHelp("↑", "previous item"),
),
Down: key.NewBinding(
key.WithKeys("down", "ctrl+n"),
key.WithHelp("↓", "next item"),
),
Enter: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "select"),
),
Escape: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "cancel"),
),
Remove: key.NewBinding(
key.WithKeys("ctrl+x"),
key.WithHelp("ctrl+x", "remove from recent"),
),
}
// NewSearchDialog creates a new SearchDialog
func NewSearchDialog(placeholder string, maxVisibleHeight int) *SearchDialog {
t := theme.CurrentTheme()
bgColor := t.BackgroundElement()
textColor := t.Text()
textMutedColor := t.TextMuted()
ti := textinput.New()
ti.Placeholder = placeholder
ti.Styles.Blurred.Placeholder = styles.NewStyle().
Foreground(textMutedColor).
Background(bgColor).
Lipgloss()
ti.Styles.Blurred.Text = styles.NewStyle().
Foreground(textColor).
Background(bgColor).
Lipgloss()
ti.Styles.Focused.Placeholder = styles.NewStyle().
Foreground(textMutedColor).
Background(bgColor).
Lipgloss()
ti.Styles.Focused.Text = styles.NewStyle().
Foreground(textColor).
Background(bgColor).
Lipgloss()
ti.Styles.Focused.Prompt = styles.NewStyle().
Background(bgColor).
Lipgloss()
ti.Styles.Cursor.Color = t.Primary()
ti.VirtualCursor = true
ti.Prompt = " "
ti.CharLimit = -1
ti.Focus()
emptyList := list.NewListComponent(
list.WithItems([]list.Item{}),
list.WithMaxVisibleHeight[list.Item](maxVisibleHeight),
list.WithFallbackMessage[list.Item](" No items"),
list.WithAlphaNumericKeys[list.Item](false),
list.WithRenderFunc(
func(item list.Item, selected bool, width int, baseStyle styles.Style) string {
return item.Render(selected, width, baseStyle)
},
),
list.WithSelectableFunc(func(item list.Item) bool {
return item.Selectable()
}),
)
return &SearchDialog{
textInput: ti,
list: emptyList,
focused: true,
}
}
func (s *SearchDialog) Init() tea.Cmd {
return textinput.Blink
}
func (s *SearchDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
value := s.textInput.Value()
if value == "" {
return s, nil
}
s.textInput.Reset()
cmds = append(cmds, func() tea.Msg {
return SearchQueryChangedMsg{Query: ""}
})
}
switch {
case key.Matches(msg, searchKeys.Escape):
return s, func() tea.Msg { return SearchCancelledMsg{} }
case key.Matches(msg, searchKeys.Enter):
if selectedItem, idx := s.list.GetSelectedItem(); idx != -1 {
return s, func() tea.Msg {
return SearchSelectionMsg{Item: selectedItem, Index: idx}
}
}
case key.Matches(msg, searchKeys.Remove):
if selectedItem, idx := s.list.GetSelectedItem(); idx != -1 {
return s, func() tea.Msg {
return SearchRemoveItemMsg{Item: selectedItem, Index: idx}
}
}
case key.Matches(msg, searchKeys.Up):
var cmd tea.Cmd
listModel, cmd := s.list.Update(msg)
s.list = listModel.(list.List[list.Item])
if cmd != nil {
cmds = append(cmds, cmd)
}
case key.Matches(msg, searchKeys.Down):
var cmd tea.Cmd
listModel, cmd := s.list.Update(msg)
s.list = listModel.(list.List[list.Item])
if cmd != nil {
cmds = append(cmds, cmd)
}
default:
oldValue := s.textInput.Value()
var cmd tea.Cmd
s.textInput, cmd = s.textInput.Update(msg)
if cmd != nil {
cmds = append(cmds, cmd)
}
if newValue := s.textInput.Value(); newValue != oldValue {
cmds = append(cmds, func() tea.Msg {
return SearchQueryChangedMsg{Query: newValue}
})
}
}
}
return s, tea.Batch(cmds...)
}
func (s *SearchDialog) View() string {
s.list.SetMaxWidth(s.width)
listView := s.list.View()
listView = lipgloss.PlaceVertical(s.list.GetMaxVisibleHeight(), lipgloss.Top, listView)
textinput := s.textInput.View()
return textinput + "\n\n" + listView
}
// SetWidth sets the width of the search dialog
func (s *SearchDialog) SetWidth(width int) {
s.width = width
s.textInput.SetWidth(width - 2) // Account for padding and borders
}
// SetHeight sets the height of the search dialog
func (s *SearchDialog) SetHeight(height int) {
s.height = height
}
// SetItems updates the list items
func (s *SearchDialog) SetItems(items []list.Item) {
s.list.SetItems(items)
}
// GetQuery returns the current search query
func (s *SearchDialog) GetQuery() string {
return s.textInput.Value()
}
// SetQuery sets the search query
func (s *SearchDialog) SetQuery(query string) {
s.textInput.SetValue(query)
}
// Focus focuses the search dialog
func (s *SearchDialog) Focus() {
s.focused = true
s.textInput.Focus()
}
// Blur removes focus from the search dialog
func (s *SearchDialog) Blur() {
s.focused = false
s.textInput.Blur()
}
@@ -30,9 +30,13 @@ type sessionItem struct {
isDeleteConfirming bool isDeleteConfirming bool
} }
func (s sessionItem) Render(selected bool, width int) string { func (s sessionItem) Render(
selected bool,
width int,
isFirstInViewport bool,
baseStyle styles.Style,
) string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
baseStyle := styles.NewStyle()
var text string var text string
if s.isDeleteConfirming { if s.isDeleteConfirming {
@@ -75,6 +79,10 @@ func (s sessionItem) Render(selected bool, width int) string {
return itemStyle.Render(truncatedStr) return itemStyle.Render(truncatedStr)
} }
func (s sessionItem) Selectable() bool {
return true
}
type sessionDialog struct { type sessionDialog struct {
width int width int
height int height int
@@ -224,12 +232,19 @@ func NewSessionDialog(app *app.App) SessionDialog {
}) })
} }
// Create a generic list component
listComponent := list.NewListComponent( listComponent := list.NewListComponent(
items, list.WithItems(items),
10, // maxVisibleSessions list.WithMaxVisibleHeight[sessionItem](10),
"No sessions available", list.WithFallbackMessage[sessionItem]("No sessions available"),
true, // useAlphaNumericKeys list.WithAlphaNumericKeys[sessionItem](true),
list.WithRenderFunc(
func(item sessionItem, selected bool, width int, baseStyle styles.Style) string {
return item.Render(selected, width, false, baseStyle)
},
),
list.WithSelectableFunc(func(item sessionItem) bool {
return true
}),
) )
listComponent.SetMaxWidth(layout.Current.Container.Width - 12) listComponent.SetMaxWidth(layout.Current.Container.Width - 12)
@@ -5,6 +5,7 @@ import (
list "github.com/sst/opencode/internal/components/list" list "github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/components/modal" "github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util" "github.com/sst/opencode/internal/util"
) )
@@ -24,7 +25,7 @@ type themeDialog struct {
height int height int
modal *modal.Modal modal *modal.Modal
list list.List[list.StringItem] list list.List[list.Item]
originalTheme string originalTheme string
themeApplied bool themeApplied bool
} }
@@ -42,16 +43,18 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() { switch msg.String() {
case "enter": case "enter":
if item, idx := t.list.GetSelectedItem(); idx >= 0 { if item, idx := t.list.GetSelectedItem(); idx >= 0 {
selectedTheme := string(item) if stringItem, ok := item.(list.StringItem); ok {
if err := theme.SetTheme(selectedTheme); err != nil { selectedTheme := string(stringItem)
// status.Error(err.Error()) if err := theme.SetTheme(selectedTheme); err != nil {
return t, nil // status.Error(err.Error())
return t, nil
}
t.themeApplied = true
return t, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(ThemeSelectedMsg{ThemeName: selectedTheme}),
)
} }
t.themeApplied = true
return t, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(ThemeSelectedMsg{ThemeName: selectedTheme}),
)
} }
} }
@@ -61,11 +64,13 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
listModel, cmd := t.list.Update(msg) listModel, cmd := t.list.Update(msg)
t.list = listModel.(list.List[list.StringItem]) t.list = listModel.(list.List[list.Item])
if item, newIdx := t.list.GetSelectedItem(); newIdx >= 0 && newIdx != prevIdx { if item, newIdx := t.list.GetSelectedItem(); newIdx >= 0 && newIdx != prevIdx {
theme.SetTheme(string(item)) if stringItem, ok := item.(list.StringItem); ok {
return t, util.CmdHandler(ThemeSelectedMsg{ThemeName: string(item)}) theme.SetTheme(string(stringItem))
return t, util.CmdHandler(ThemeSelectedMsg{ThemeName: string(stringItem)})
}
} }
return t, cmd return t, cmd
} }
@@ -94,21 +99,32 @@ func NewThemeDialog() ThemeDialog {
} }
} }
list := list.NewStringList( // Convert themes to list items
themes, items := make([]list.Item, len(themes))
10, // maxVisibleThemes for i, theme := range themes {
"No themes available", items[i] = list.StringItem(theme)
true, }
listComponent := list.NewListComponent(
list.WithItems(items),
list.WithMaxVisibleHeight[list.Item](10),
list.WithFallbackMessage[list.Item]("No themes available"),
list.WithAlphaNumericKeys[list.Item](true),
list.WithRenderFunc(func(item list.Item, selected bool, width int, baseStyle styles.Style) string {
return item.Render(selected, width, baseStyle)
}),
list.WithSelectableFunc(func(item list.Item) bool {
return item.Selectable()
}),
) )
// Set the initial selection to the current theme // Set the initial selection to the current theme
list.SetSelectedIndex(selectedIdx) listComponent.SetSelectedIndex(selectedIdx)
// Set the max width for the list to match the modal width // Set the max width for the list to match the modal width
list.SetMaxWidth(36) // 40 (modal max width) - 4 (modal padding) listComponent.SetMaxWidth(36) // 40 (modal max width) - 4 (modal padding)
return &themeDialog{ return &themeDialog{
list: list, list: listComponent,
modal: modal.New(modal.WithTitle("Select Theme"), modal.WithMaxWidth(40)), modal: modal.New(modal.WithTitle("Select Theme"), modal.WithMaxWidth(40)),
originalTheme: currentTheme, originalTheme: currentTheme,
themeApplied: false, themeApplied: false,
+272 -54
View File
@@ -5,16 +5,88 @@ import (
"github.com/charmbracelet/bubbles/v2/key" "github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/muesli/reflow/truncate" "github.com/muesli/reflow/truncate"
"github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
) )
type ListItem interface { // Item interface that all list items must implement
Render(selected bool, width int) string type Item interface {
Render(selected bool, width int, baseStyle styles.Style) string
Selectable() bool
} }
type List[T ListItem] interface { // RenderFunc defines how to render an item in the list
type RenderFunc[T any] func(item T, selected bool, width int, baseStyle styles.Style) string
// SelectableFunc defines whether an item is selectable
type SelectableFunc[T any] func(item T) bool
// Options holds configuration for the list component
type Options[T any] struct {
items []T
maxVisibleHeight int
fallbackMsg string
useAlphaNumericKeys bool
renderItem RenderFunc[T]
isSelectable SelectableFunc[T]
baseStyle styles.Style
}
// Option is a function that configures the list component
type Option[T any] func(*Options[T])
// WithItems sets the initial items for the list
func WithItems[T any](items []T) Option[T] {
return func(o *Options[T]) {
o.items = items
}
}
// WithMaxVisibleHeight sets the maximum visible height in lines
func WithMaxVisibleHeight[T any](height int) Option[T] {
return func(o *Options[T]) {
o.maxVisibleHeight = height
}
}
// WithFallbackMessage sets the message to show when the list is empty
func WithFallbackMessage[T any](msg string) Option[T] {
return func(o *Options[T]) {
o.fallbackMsg = msg
}
}
// WithAlphaNumericKeys enables j/k navigation keys
func WithAlphaNumericKeys[T any](enabled bool) Option[T] {
return func(o *Options[T]) {
o.useAlphaNumericKeys = enabled
}
}
// WithRenderFunc sets the function to render items
func WithRenderFunc[T any](fn RenderFunc[T]) Option[T] {
return func(o *Options[T]) {
o.renderItem = fn
}
}
// WithSelectableFunc sets the function to determine if items are selectable
func WithSelectableFunc[T any](fn SelectableFunc[T]) Option[T] {
return func(o *Options[T]) {
o.isSelectable = fn
}
}
// WithStyle sets the base style that gets passed to render functions
func WithStyle[T any](style styles.Style) Option[T] {
return func(o *Options[T]) {
o.baseStyle = style
}
}
type List[T any] interface {
tea.Model tea.Model
tea.ViewModel tea.ViewModel
SetMaxWidth(maxWidth int) SetMaxWidth(maxWidth int)
@@ -24,17 +96,21 @@ type List[T ListItem] interface {
SetSelectedIndex(idx int) SetSelectedIndex(idx int)
SetEmptyMessage(msg string) SetEmptyMessage(msg string)
IsEmpty() bool IsEmpty() bool
GetMaxVisibleHeight() int
} }
type listComponent[T ListItem] struct { type listComponent[T any] struct {
fallbackMsg string fallbackMsg string
items []T items []T
selectedIdx int selectedIdx int
maxWidth int maxWidth int
maxVisibleItems int maxVisibleHeight int
useAlphaNumericKeys bool useAlphaNumericKeys bool
width int width int
height int height int
renderItem RenderFunc[T]
isSelectable SelectableFunc[T]
baseStyle styles.Style
} }
type listKeyMap struct { type listKeyMap struct {
@@ -46,11 +122,11 @@ type listKeyMap struct {
var simpleListKeys = listKeyMap{ var simpleListKeys = listKeyMap{
Up: key.NewBinding( Up: key.NewBinding(
key.WithKeys("up"), key.WithKeys("up", "ctrl+p"),
key.WithHelp("↑", "previous list item"), key.WithHelp("↑", "previous list item"),
), ),
Down: key.NewBinding( Down: key.NewBinding(
key.WithKeys("down"), key.WithKeys("down", "ctrl+n"),
key.WithHelp("↓", "next list item"), key.WithHelp("↓", "next list item"),
), ),
UpAlpha: key.NewBinding( UpAlpha: key.NewBinding(
@@ -72,14 +148,10 @@ func (c *listComponent[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg: case tea.KeyMsg:
switch { switch {
case key.Matches(msg, simpleListKeys.Up) || (c.useAlphaNumericKeys && key.Matches(msg, simpleListKeys.UpAlpha)): case key.Matches(msg, simpleListKeys.Up) || (c.useAlphaNumericKeys && key.Matches(msg, simpleListKeys.UpAlpha)):
if c.selectedIdx > 0 { c.moveUp()
c.selectedIdx--
}
return c, nil return c, nil
case key.Matches(msg, simpleListKeys.Down) || (c.useAlphaNumericKeys && key.Matches(msg, simpleListKeys.DownAlpha)): case key.Matches(msg, simpleListKeys.Down) || (c.useAlphaNumericKeys && key.Matches(msg, simpleListKeys.DownAlpha)):
if c.selectedIdx < len(c.items)-1 { c.moveDown()
c.selectedIdx++
}
return c, nil return c, nil
} }
} }
@@ -87,8 +159,50 @@ func (c *listComponent[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return c, nil return c, nil
} }
// moveUp moves the selection up, skipping non-selectable items
func (c *listComponent[T]) moveUp() {
if len(c.items) == 0 {
return
}
// Find the previous selectable item
for i := c.selectedIdx - 1; i >= 0; i-- {
if c.isSelectable(c.items[i]) {
c.selectedIdx = i
return
}
}
// If no selectable item found above, stay at current position
}
// moveDown moves the selection down, skipping non-selectable items
func (c *listComponent[T]) moveDown() {
if len(c.items) == 0 {
return
}
originalIdx := c.selectedIdx
for {
if c.selectedIdx < len(c.items)-1 {
c.selectedIdx++
} else {
break
}
if c.isSelectable(c.items[c.selectedIdx]) {
return
}
// Prevent infinite loop
if c.selectedIdx == originalIdx {
break
}
}
}
func (c *listComponent[T]) GetSelectedItem() (T, int) { func (c *listComponent[T]) GetSelectedItem() (T, int) {
if len(c.items) > 0 { if len(c.items) > 0 && c.isSelectable(c.items[c.selectedIdx]) {
return c.items[c.selectedIdx], c.selectedIdx return c.items[c.selectedIdx], c.selectedIdx
} }
@@ -97,8 +211,13 @@ func (c *listComponent[T]) GetSelectedItem() (T, int) {
} }
func (c *listComponent[T]) SetItems(items []T) { func (c *listComponent[T]) SetItems(items []T) {
c.selectedIdx = 0
c.items = items c.items = items
c.selectedIdx = 0
// Ensure initial selection is on a selectable item
if len(items) > 0 && !c.isSelectable(items[0]) {
c.moveDown()
}
} }
func (c *listComponent[T]) GetItems() []T { func (c *listComponent[T]) GetItems() []T {
@@ -123,62 +242,145 @@ func (c *listComponent[T]) SetSelectedIndex(idx int) {
} }
} }
func (c *listComponent[T]) GetMaxVisibleHeight() int {
return c.maxVisibleHeight
}
func (c *listComponent[T]) View() string { func (c *listComponent[T]) View() string {
items := c.items items := c.items
maxWidth := c.maxWidth maxWidth := c.maxWidth
if maxWidth == 0 { if maxWidth == 0 {
maxWidth = 80 // Default width if not set maxWidth = 80 // Default width if not set
} }
maxVisibleItems := min(c.maxVisibleItems, len(items))
startIdx := 0
if len(items) <= 0 { if len(items) <= 0 {
return c.fallbackMsg return c.fallbackMsg
} }
if len(items) > maxVisibleItems { // Calculate viewport based on actual heights
halfVisible := maxVisibleItems / 2 startIdx, endIdx := c.calculateViewport()
if c.selectedIdx >= halfVisible && c.selectedIdx < len(items)-halfVisible {
startIdx = c.selectedIdx - halfVisible
} else if c.selectedIdx >= len(items)-halfVisible {
startIdx = len(items) - maxVisibleItems
}
}
endIdx := min(startIdx+maxVisibleItems, len(items)) listItems := make([]string, 0, endIdx-startIdx)
listItems := make([]string, 0, maxVisibleItems)
for i := startIdx; i < endIdx; i++ { for i := startIdx; i < endIdx; i++ {
item := items[i] item := items[i]
title := item.Render(i == c.selectedIdx, maxWidth)
// Special handling for HeaderItem to remove top margin on first item
if i == startIdx {
// Check if this is a HeaderItem
if _, ok := any(item).(Item); ok {
if headerItem, isHeader := any(item).(HeaderItem); isHeader {
// Render header without top margin when it's first
t := theme.CurrentTheme()
truncatedStr := truncate.StringWithTail(string(headerItem), uint(maxWidth-1), "...")
headerStyle := c.baseStyle.
Foreground(t.Accent()).
Bold(true).
MarginBottom(0).
PaddingLeft(1)
listItems = append(listItems, headerStyle.Render(truncatedStr))
continue
}
}
}
title := c.renderItem(item, i == c.selectedIdx, maxWidth, c.baseStyle)
listItems = append(listItems, title) listItems = append(listItems, title)
} }
return strings.Join(listItems, "\n") return strings.Join(listItems, "\n")
} }
func NewListComponent[T ListItem]( // calculateViewport determines which items to show based on available space
items []T, func (c *listComponent[T]) calculateViewport() (startIdx, endIdx int) {
maxVisibleItems int, items := c.items
fallbackMsg string, if len(items) == 0 {
useAlphaNumericKeys bool, return 0, 0
) List[T] { }
// Calculate heights of all items
itemHeights := make([]int, len(items))
for i, item := range items {
rendered := c.renderItem(item, false, c.maxWidth, c.baseStyle)
itemHeights[i] = lipgloss.Height(rendered)
}
// Find the range of items that fit within maxVisibleHeight
// Start by trying to center the selected item
start := 0
end := len(items)
// Calculate height from start to selected
heightToSelected := 0
for i := 0; i <= c.selectedIdx && i < len(items); i++ {
heightToSelected += itemHeights[i]
}
// If selected item is beyond visible height, scroll to show it
if heightToSelected > c.maxVisibleHeight {
// Start from selected and work backwards to find start
currentHeight := itemHeights[c.selectedIdx]
start = c.selectedIdx
for i := c.selectedIdx - 1; i >= 0 && currentHeight+itemHeights[i] <= c.maxVisibleHeight; i-- {
currentHeight += itemHeights[i]
start = i
}
}
// Calculate end based on start
currentHeight := 0
for i := start; i < len(items); i++ {
if currentHeight+itemHeights[i] > c.maxVisibleHeight {
end = i
break
}
currentHeight += itemHeights[i]
}
return start, end
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func NewListComponent[T any](opts ...Option[T]) List[T] {
options := &Options[T]{
baseStyle: styles.NewStyle(), // Default empty style
}
for _, opt := range opts {
opt(options)
}
return &listComponent[T]{ return &listComponent[T]{
fallbackMsg: fallbackMsg, fallbackMsg: options.fallbackMsg,
items: items, items: options.items,
maxVisibleItems: maxVisibleItems, maxVisibleHeight: options.maxVisibleHeight,
useAlphaNumericKeys: useAlphaNumericKeys, useAlphaNumericKeys: options.useAlphaNumericKeys,
selectedIdx: 0, selectedIdx: 0,
renderItem: options.renderItem,
isSelectable: options.isSelectable,
baseStyle: options.baseStyle,
} }
} }
// StringItem is a simple implementation of ListItem for string values // StringItem is a simple implementation of Item for string values
type StringItem string type StringItem string
func (s StringItem) Render(selected bool, width int) string { func (s StringItem) Render(selected bool, width int, baseStyle styles.Style) string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
baseStyle := styles.NewStyle()
truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...") truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...")
@@ -198,16 +400,32 @@ func (s StringItem) Render(selected bool, width int) string {
return itemStyle.Render(truncatedStr) return itemStyle.Render(truncatedStr)
} }
// NewStringList creates a new list component with string items func (s StringItem) Selectable() bool {
func NewStringList( return true
items []string,
maxVisibleItems int,
fallbackMsg string,
useAlphaNumericKeys bool,
) List[StringItem] {
stringItems := make([]StringItem, len(items))
for i, item := range items {
stringItems[i] = StringItem(item)
}
return NewListComponent(stringItems, maxVisibleItems, fallbackMsg, useAlphaNumericKeys)
} }
// HeaderItem is a non-selectable header item for grouping
type HeaderItem string
func (h HeaderItem) Render(selected bool, width int, baseStyle styles.Style) string {
t := theme.CurrentTheme()
truncatedStr := truncate.StringWithTail(string(h), uint(width-1), "...")
headerStyle := baseStyle.
Foreground(t.Accent()).
Bold(true).
MarginTop(1).
MarginBottom(0).
PaddingLeft(1)
return headerStyle.Render(truncatedStr)
}
func (h HeaderItem) Selectable() bool {
return false
}
// Ensure StringItem and HeaderItem implement Item
var _ Item = StringItem("")
var _ Item = HeaderItem("")
@@ -0,0 +1,219 @@
package list
import (
"testing"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/sst/opencode/internal/styles"
)
// testItem is a simple test implementation of ListItem
type testItem struct {
value string
}
func (t testItem) Render(
selected bool,
width int,
isFirstInViewport bool,
baseStyle styles.Style,
) string {
return t.value
}
func (t testItem) Selectable() bool {
return true
}
// createTestList creates a list with test items for testing
func createTestList() *listComponent[testItem] {
items := []testItem{
{value: "item1"},
{value: "item2"},
{value: "item3"},
}
list := NewListComponent(
WithItems(items),
WithMaxVisibleItems[testItem](5),
WithFallbackMessage[testItem]("empty"),
WithAlphaNumericKeys[testItem](false),
WithRenderFunc(
func(item testItem, selected bool, width int, baseStyle styles.Style) string {
return item.Render(selected, width, false, baseStyle)
},
),
WithSelectableFunc(func(item testItem) bool {
return item.Selectable()
}),
WithHeightFunc(func(item testItem, isFirstInViewport bool) int {
return 1
}),
)
return list.(*listComponent[testItem])
}
func TestArrowKeyNavigation(t *testing.T) {
list := createTestList()
// Test down arrow navigation
downKey := tea.KeyPressMsg{Code: tea.KeyDown}
updatedModel, _ := list.Update(downKey)
list = updatedModel.(*listComponent[testItem])
_, idx := list.GetSelectedItem()
if idx != 1 {
t.Errorf("Expected selected index 1 after down arrow, got %d", idx)
}
// Test up arrow navigation
upKey := tea.KeyPressMsg{Code: tea.KeyUp}
updatedModel, _ = list.Update(upKey)
list = updatedModel.(*listComponent[testItem])
_, idx = list.GetSelectedItem()
if idx != 0 {
t.Errorf("Expected selected index 0 after up arrow, got %d", idx)
}
}
func TestJKKeyNavigation(t *testing.T) {
items := []testItem{
{value: "item1"},
{value: "item2"},
{value: "item3"},
}
// Create list with alpha keys enabled
list := NewListComponent(
WithItems(items),
WithMaxVisibleItems[testItem](5),
WithFallbackMessage[testItem]("empty"),
WithAlphaNumericKeys[testItem](true),
WithRenderFunc(
func(item testItem, selected bool, width int, baseStyle styles.Style) string {
return item.Render(selected, width, false, baseStyle)
},
),
WithSelectableFunc(func(item testItem) bool {
return item.Selectable()
}),
WithHeightFunc(func(item testItem, isFirstInViewport bool) int {
return 1
}),
)
// Test j key (down)
jKey := tea.KeyPressMsg{Code: 'j', Text: "j"}
updatedModel, _ := list.Update(jKey)
list = updatedModel.(*listComponent[testItem])
_, idx := list.GetSelectedItem()
if idx != 1 {
t.Errorf("Expected selected index 1 after 'j' key, got %d", idx)
}
// Test k key (up)
kKey := tea.KeyPressMsg{Code: 'k', Text: "k"}
updatedModel, _ = list.Update(kKey)
list = updatedModel.(*listComponent[testItem])
_, idx = list.GetSelectedItem()
if idx != 0 {
t.Errorf("Expected selected index 0 after 'k' key, got %d", idx)
}
}
func TestCtrlNavigation(t *testing.T) {
list := createTestList()
// Test Ctrl-N (down)
ctrlN := tea.KeyPressMsg{Code: 'n', Mod: tea.ModCtrl}
updatedModel, _ := list.Update(ctrlN)
list = updatedModel.(*listComponent[testItem])
_, idx := list.GetSelectedItem()
if idx != 1 {
t.Errorf("Expected selected index 1 after Ctrl-N, got %d", idx)
}
// Test Ctrl-P (up)
ctrlP := tea.KeyPressMsg{Code: 'p', Mod: tea.ModCtrl}
updatedModel, _ = list.Update(ctrlP)
list = updatedModel.(*listComponent[testItem])
_, idx = list.GetSelectedItem()
if idx != 0 {
t.Errorf("Expected selected index 0 after Ctrl-P, got %d", idx)
}
}
func TestNavigationBoundaries(t *testing.T) {
list := createTestList()
// Test up arrow at first item (should stay at 0)
upKey := tea.KeyPressMsg{Code: tea.KeyUp}
updatedModel, _ := list.Update(upKey)
list = updatedModel.(*listComponent[testItem])
_, idx := list.GetSelectedItem()
if idx != 0 {
t.Errorf("Expected to stay at index 0 when pressing up at first item, got %d", idx)
}
// Move to last item
downKey := tea.KeyPressMsg{Code: tea.KeyDown}
updatedModel, _ = list.Update(downKey)
list = updatedModel.(*listComponent[testItem])
updatedModel, _ = list.Update(downKey)
list = updatedModel.(*listComponent[testItem])
_, idx = list.GetSelectedItem()
if idx != 2 {
t.Errorf("Expected to be at index 2, got %d", idx)
}
// Test down arrow at last item (should stay at 2)
updatedModel, _ = list.Update(downKey)
list = updatedModel.(*listComponent[testItem])
_, idx = list.GetSelectedItem()
if idx != 2 {
t.Errorf("Expected to stay at index 2 when pressing down at last item, got %d", idx)
}
}
func TestEmptyList(t *testing.T) {
emptyList := NewListComponent(
WithItems([]testItem{}),
WithMaxVisibleItems[testItem](5),
WithFallbackMessage[testItem]("empty"),
WithAlphaNumericKeys[testItem](false),
WithRenderFunc(
func(item testItem, selected bool, width int, baseStyle styles.Style) string {
return item.Render(selected, width, false, baseStyle)
},
),
WithSelectableFunc(func(item testItem) bool {
return item.Selectable()
}),
WithHeightFunc(func(item testItem, isFirstInViewport bool) int {
return 1
}),
)
// Test navigation on empty list (should not crash)
downKey := tea.KeyPressMsg{Code: tea.KeyDown}
upKey := tea.KeyPressMsg{Code: tea.KeyUp}
ctrlN := tea.KeyPressMsg{Code: 'n', Mod: tea.ModCtrl}
ctrlP := tea.KeyPressMsg{Code: 'p', Mod: tea.ModCtrl}
updatedModel, _ := emptyList.Update(downKey)
emptyList = updatedModel.(*listComponent[testItem])
updatedModel, _ = emptyList.Update(upKey)
emptyList = updatedModel.(*listComponent[testItem])
updatedModel, _ = emptyList.Update(ctrlN)
emptyList = updatedModel.(*listComponent[testItem])
updatedModel, _ = emptyList.Update(ctrlP)
emptyList = updatedModel.(*listComponent[testItem])
// Verify empty list behavior
_, idx := emptyList.GetSelectedItem()
if idx != -1 {
t.Errorf("Expected index -1 for empty list, got %d", idx)
}
if !emptyList.IsEmpty() {
t.Error("Expected IsEmpty() to return true for empty list")
}
}
@@ -95,7 +95,7 @@ func (m *Modal) Render(contentView string, background string) string {
var finalContent string var finalContent string
if m.title != "" { if m.title != "" {
titleStyle := baseStyle. titleStyle := baseStyle.
Foreground(t.Primary()). Foreground(t.Text()).
Bold(true). Bold(true).
Padding(0, 1) Padding(0, 1)
@@ -140,6 +140,6 @@ func (m *Modal) Render(contentView string, background string) string {
modalView, modalView,
background, background,
layout.WithOverlayBorder(), layout.WithOverlayBorder(),
layout.WithOverlayBorderColor(t.Primary()), layout.WithOverlayBorderColor(t.BorderActive()),
) )
} }
+9
View File
@@ -69,6 +69,15 @@ func (s *State) UpdateModelUsage(providerID, modelID string) {
} }
} }
func (s *State) RemoveModelFromRecentlyUsed(providerID, modelID string) {
for i, usage := range s.RecentlyUsedModels {
if usage.ProviderID == providerID && usage.ModelID == modelID {
s.RecentlyUsedModels = append(s.RecentlyUsedModels[:i], s.RecentlyUsedModels[i+1:]...)
return
}
}
}
// SaveState writes the provided Config struct to the specified TOML file. // SaveState writes the provided Config struct to the specified TOML file.
// It will create the file if it doesn't exist, or overwrite it if it does. // It will create the file if it doesn't exist, or overwrite it if it does.
func SaveState(filePath string, state *State) error { func SaveState(filePath string, state *State) error {
+107 -15
View File
@@ -2,6 +2,7 @@ package tui
import ( import (
"context" "context"
"fmt"
"log/slog" "log/slog"
"os" "os"
"os/exec" "os/exec"
@@ -65,11 +66,10 @@ type appModel struct {
editor chat.EditorComponent editor chat.EditorComponent
messages chat.MessagesComponent messages chat.MessagesComponent
completions dialog.CompletionDialog completions dialog.CompletionDialog
commandProvider dialog.CompletionProvider commandProvider completions.CompletionProvider
fileProvider dialog.CompletionProvider fileProvider completions.CompletionProvider
symbolsProvider dialog.CompletionProvider symbolsProvider completions.CompletionProvider
showCompletionDialog bool showCompletionDialog bool
fileCompletionActive bool
leaderBinding *key.Binding leaderBinding *key.Binding
// isLeaderSequence bool // isLeaderSequence bool
toastManager *toast.ToastManager toastManager *toast.ToastManager
@@ -115,6 +115,11 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyPressMsg: case tea.KeyPressMsg:
keyString := msg.String() keyString := msg.String()
// Handle Ctrl+Z for suspend
if keyString == "ctrl+z" {
return a, tea.Suspend
}
// 1. Handle active modal // 1. Handle active modal
if a.modal != nil { if a.modal != nil {
switch keyString { switch keyString {
@@ -155,7 +160,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
!a.showCompletionDialog && !a.showCompletionDialog &&
a.editor.Value() == "" { a.editor.Value() == "" {
a.showCompletionDialog = true a.showCompletionDialog = true
a.fileCompletionActive = false
updated, cmd := a.editor.Update(msg) updated, cmd := a.editor.Update(msg)
a.editor = updated.(chat.EditorComponent) a.editor = updated.(chat.EditorComponent)
@@ -174,7 +178,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if keyString == "@" && if keyString == "@" &&
!a.showCompletionDialog { !a.showCompletionDialog {
a.showCompletionDialog = true a.showCompletionDialog = true
a.fileCompletionActive = true
updated, cmd := a.editor.Update(msg) updated, cmd := a.editor.Update(msg)
a.editor = updated.(chat.EditorComponent) a.editor = updated.(chat.EditorComponent)
@@ -191,7 +194,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if a.showCompletionDialog { if a.showCompletionDialog {
switch keyString { switch keyString {
case "tab", "enter", "esc", "ctrl+c", "up", "down": case "tab", "enter", "esc", "ctrl+c", "up", "down", "ctrl+p", "ctrl+n":
updated, cmd := a.completions.Update(msg) updated, cmd := a.completions.Update(msg)
a.completions = updated.(dialog.CompletionDialog) a.completions = updated.(dialog.CompletionDialog)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
@@ -356,7 +359,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
case dialog.CompletionDialogCloseMsg: case dialog.CompletionDialogCloseMsg:
a.showCompletionDialog = false a.showCompletionDialog = false
a.fileCompletionActive = false
case opencode.EventListResponseEventInstallationUpdated: case opencode.EventListResponseEventInstallationUpdated:
return a, toast.NewSuccessToast( return a, toast.NewSuccessToast(
"opencode updated to "+msg.Properties.Version+", restart to apply.", "opencode updated to "+msg.Properties.Version+", restart to apply.",
@@ -456,12 +458,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
msg.Height -= 2 // Make space for the status bar msg.Height -= 2 // Make space for the status bar
a.width, a.height = msg.Width, msg.Height a.width, a.height = msg.Width, msg.Height
container := min(a.width, 84) container := min(a.width, 104)
if a.fileViewer.HasFile() { if a.fileViewer.HasFile() {
if a.width < fileViewerFullWidthCutoff { if a.width < fileViewerFullWidthCutoff {
container = a.width container = a.width
} else { } else {
container = min(min(a.width, max(a.width/2, 50)), 84) container = min(min(a.width, max(a.width/2, 50)), 104)
} }
} }
layout.Current = &layout.LayoutInfo{ layout.Current = &layout.LayoutInfo{
@@ -491,12 +493,15 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case app.SessionSelectedMsg: case app.SessionSelectedMsg:
messages, err := a.app.ListMessages(context.Background(), msg.ID) messages, err := a.app.ListMessages(context.Background(), msg.ID)
if err != nil { if err != nil {
slog.Error("Failed to list messages", "error", err) slog.Error("Failed to list messages", "error", err.Error())
return a, toast.NewErrorToast("Failed to open session") return a, toast.NewErrorToast("Failed to open session")
} }
a.app.Session = msg a.app.Session = msg
a.app.Messages = messages a.app.Messages = messages
return a, util.CmdHandler(app.SessionLoadedMsg{}) return a, util.CmdHandler(app.SessionLoadedMsg{})
case app.SessionCreatedMsg:
a.app.Session = msg.Session
return a, util.CmdHandler(app.SessionLoadedMsg{})
case app.ModelSelectedMsg: case app.ModelSelectedMsg:
a.app.Provider = &msg.Provider a.app.Provider = &msg.Provider
a.app.Model = &msg.Model a.app.Model = &msg.Model
@@ -896,6 +901,56 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
} }
// TODO: block until compaction is complete // TODO: block until compaction is complete
a.app.CompactSession(context.Background()) a.app.CompactSession(context.Background())
case commands.SessionExportCommand:
if a.app.Session.ID == "" {
return a, toast.NewErrorToast("No active session to export.")
}
// Use current conversation history
messages := a.app.Messages
if len(messages) == 0 {
return a, toast.NewInfoToast("No messages to export.")
}
// Format to Markdown
markdownContent := formatConversationToMarkdown(messages)
// Check if EDITOR is set
editor := os.Getenv("EDITOR")
if editor == "" {
return a, toast.NewErrorToast("No EDITOR set, can't open editor")
}
// Create and write to temp file
tmpfile, err := os.CreateTemp("", "conversation-*.md")
if err != nil {
slog.Error("Failed to create temp file", "error", err)
return a, toast.NewErrorToast("Failed to create temporary file.")
}
_, err = tmpfile.WriteString(markdownContent)
if err != nil {
slog.Error("Failed to write to temp file", "error", err)
tmpfile.Close()
os.Remove(tmpfile.Name())
return a, toast.NewErrorToast("Failed to write conversation to file.")
}
tmpfile.Close()
// Open in editor
c := exec.Command(editor, tmpfile.Name())
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
cmd = tea.ExecProcess(c, func(err error) tea.Msg {
if err != nil {
slog.Error("Failed to open editor for conversation", "error", err)
}
// Clean up the file after editor closes
os.Remove(tmpfile.Name())
return nil
})
cmds = append(cmds, cmd)
case commands.ToolDetailsCommand: case commands.ToolDetailsCommand:
message := "Tool details are now visible" message := "Tool details are now visible"
if a.messages.ToolDetailsVisible() { if a.messages.ToolDetailsVisible() {
@@ -911,9 +966,8 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
a.modal = themeDialog a.modal = themeDialog
case commands.FileListCommand: case commands.FileListCommand:
a.editor.Blur() a.editor.Blur()
provider := completions.NewFileContextGroup(a.app) findDialog := dialog.NewFindDialog(a.fileProvider)
findDialog := dialog.NewFindDialog(provider) cmds = append(cmds, findDialog.Init())
findDialog.SetWidth(layout.Current.Container.Width - 8)
a.modal = findDialog a.modal = findDialog
case commands.FileCloseCommand: case commands.FileCloseCommand:
a.fileViewer, cmd = a.fileViewer.Clear() a.fileViewer, cmd = a.fileViewer.Clear()
@@ -1043,7 +1097,6 @@ func NewModel(app *app.App) tea.Model {
symbolsProvider: symbolsProvider, symbolsProvider: symbolsProvider,
leaderBinding: leaderBinding, leaderBinding: leaderBinding,
showCompletionDialog: false, showCompletionDialog: false,
fileCompletionActive: false,
toastManager: toast.NewToastManager(), toastManager: toast.NewToastManager(),
interruptKeyState: InterruptKeyIdle, interruptKeyState: InterruptKeyIdle,
exitKeyState: ExitKeyIdle, exitKeyState: ExitKeyIdle,
@@ -1053,3 +1106,42 @@ func NewModel(app *app.App) tea.Model {
return model return model
} }
func formatConversationToMarkdown(messages []app.Message) string {
var builder strings.Builder
builder.WriteString("# Conversation History\n\n")
for _, msg := range messages {
builder.WriteString("---\n\n")
var role string
var timestamp time.Time
switch info := msg.Info.(type) {
case opencode.UserMessage:
role = "User"
timestamp = time.UnixMilli(int64(info.Time.Created))
case opencode.AssistantMessage:
role = "Assistant"
timestamp = time.UnixMilli(int64(info.Time.Created))
default:
continue
}
builder.WriteString(fmt.Sprintf("**%s** (*%s*)\n\n", role, timestamp.Format("2006-01-02 15:04:05")))
for _, part := range msg.Parts {
switch p := part.(type) {
case opencode.TextPart:
builder.WriteString("> " + strings.ReplaceAll(p.Text, "\n", "\n> ") + "\n\n")
case opencode.FilePart:
builder.WriteString(fmt.Sprintf("> [File: %s]\n\n", p.Filename))
case opencode.ToolPart:
builder.WriteString(fmt.Sprintf("> [Tool: %s]\n\n", p.Tool))
}
}
}
return builder.String()
}
+3 -3
View File
@@ -1,4 +1,4 @@
configured_endpoints: 22 configured_endpoints: 22
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-352994eb17f76d9472b0f0176efacf77a200a6fab2db28d1cfcd29451b211d7a.yml openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-7270b9e4859010d6680bcc92afcd6f7c679d80a2645f65d7097d19ce2e8cdc5a.yml
openapi_spec_hash: f01cd3de8c7cf0c9fd513896e81986de openapi_spec_hash: 5fcbfaedebfea62c17c74437a9728b04
config_hash: 3695cfc829cfaae14490850b4a1ed282 config_hash: 38041c37df28a1c4383718e6d148dd0a
+12 -15
View File
@@ -49,14 +49,11 @@ import (
func main() { func main() {
client := opencode.NewClient() client := opencode.NewClient()
stream := client.Event.ListStreaming(context.TODO()) sessions, err := client.Session.List(context.TODO())
for stream.Next() {
fmt.Printf("%+v\n", stream.Current())
}
err := stream.Err()
if err != nil { if err != nil {
panic(err.Error()) panic(err.Error())
} }
fmt.Printf("%+v\n", sessions)
} }
``` ```
@@ -145,7 +142,7 @@ client := opencode.NewClient(
option.WithHeader("X-Some-Header", "custom_header_info"), option.WithHeader("X-Some-Header", "custom_header_info"),
) )
client.Event.List(context.TODO(), ..., client.Session.List(context.TODO(), ...,
// Override the header // Override the header
option.WithHeader("X-Some-Header", "some_other_custom_header_info"), option.WithHeader("X-Some-Header", "some_other_custom_header_info"),
// Add an undocumented field to the request body, using sjson syntax // Add an undocumented field to the request body, using sjson syntax
@@ -174,14 +171,14 @@ When the API returns a non-success status code, we return an error with type
To handle errors, we recommend that you use the `errors.As` pattern: To handle errors, we recommend that you use the `errors.As` pattern:
```go ```go
stream := client.Event.ListStreaming(context.TODO()) _, err := client.Session.List(context.TODO())
if stream.Err() != nil { if err != nil {
var apierr *opencode.Error var apierr *opencode.Error
if errors.As(stream.Err(), &apierr) { if errors.As(err, &apierr) {
println(string(apierr.DumpRequest(true))) // Prints the serialized HTTP request println(string(apierr.DumpRequest(true))) // Prints the serialized HTTP request
println(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response println(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response
} }
panic(stream.Err().Error()) // GET "/event": 400 Bad Request { ... } panic(err.Error()) // GET "/session": 400 Bad Request { ... }
} }
``` ```
@@ -199,7 +196,7 @@ To set a per-retry timeout, use `option.WithRequestTimeout()`.
// This sets the timeout for the request, including all the retries. // This sets the timeout for the request, including all the retries.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel() defer cancel()
client.Event.ListStreaming( client.Session.List(
ctx, ctx,
// This sets the per-retry timeout // This sets the per-retry timeout
option.WithRequestTimeout(20*time.Second), option.WithRequestTimeout(20*time.Second),
@@ -234,7 +231,7 @@ client := opencode.NewClient(
) )
// Override per-request: // Override per-request:
client.Event.ListStreaming(context.TODO(), option.WithMaxRetries(5)) client.Session.List(context.TODO(), option.WithMaxRetries(5))
``` ```
### Accessing raw response data (e.g. response headers) ### Accessing raw response data (e.g. response headers)
@@ -245,11 +242,11 @@ you need to examine response headers, status codes, or other details.
```go ```go
// Create a variable to store the HTTP response // Create a variable to store the HTTP response
var response *http.Response var response *http.Response
stream := client.Event.ListStreaming(context.TODO(), option.WithResponseInto(&response)) sessions, err := client.Session.List(context.TODO(), option.WithResponseInto(&response))
if stream.Err() != nil { if err != nil {
// handle error // handle error
} }
fmt.Printf("%+v\n", events) fmt.Printf("%+v\n", sessions)
fmt.Printf("Status Code: %d\n", response.StatusCode) fmt.Printf("Status Code: %d\n", response.StatusCode)
fmt.Printf("Headers: %+#v\n", response.Header) fmt.Printf("Headers: %+#v\n", response.Header)
+1
View File
@@ -85,6 +85,7 @@ Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a> - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Part">Part</a> - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Part">Part</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a> - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SnapshotPart">SnapshotPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepFinishPart">StepFinishPart</a> - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepFinishPart">StepFinishPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepStartPart">StepStartPart</a> - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepStartPart">StepStartPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPart">TextPart</a> - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPart">TextPart</a>
-2
View File
@@ -68,7 +68,6 @@ type App struct {
Hostname string `json:"hostname,required"` Hostname string `json:"hostname,required"`
Path AppPath `json:"path,required"` Path AppPath `json:"path,required"`
Time AppTime `json:"time,required"` Time AppTime `json:"time,required"`
User string `json:"user,required"`
JSON appJSON `json:"-"` JSON appJSON `json:"-"`
} }
@@ -78,7 +77,6 @@ type appJSON struct {
Hostname apijson.Field Hostname apijson.Field
Path apijson.Field Path apijson.Field
Time apijson.Field Time apijson.Field
User apijson.Field
raw string raw string
ExtraFields map[string]apijson.Field ExtraFields map[string]apijson.Field
} }
+8 -36
View File
@@ -38,7 +38,7 @@ func TestUserAgentHeader(t *testing.T) {
}, },
}), }),
) )
client.Event.ListStreaming(context.Background()) client.Session.List(context.Background())
if userAgent != fmt.Sprintf("Opencode/Go %s", internal.PackageVersion) { if userAgent != fmt.Sprintf("Opencode/Go %s", internal.PackageVersion) {
t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent) t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent)
} }
@@ -61,11 +61,7 @@ func TestRetryAfter(t *testing.T) {
}, },
}), }),
) )
stream := client.Event.ListStreaming(context.Background()) _, err := client.Session.List(context.Background())
for stream.Next() {
// ...
}
err := stream.Err()
if err == nil { if err == nil {
t.Error("Expected there to be a cancel error") t.Error("Expected there to be a cancel error")
} }
@@ -99,11 +95,7 @@ func TestDeleteRetryCountHeader(t *testing.T) {
}), }),
option.WithHeaderDel("X-Stainless-Retry-Count"), option.WithHeaderDel("X-Stainless-Retry-Count"),
) )
stream := client.Event.ListStreaming(context.Background()) _, err := client.Session.List(context.Background())
for stream.Next() {
// ...
}
err := stream.Err()
if err == nil { if err == nil {
t.Error("Expected there to be a cancel error") t.Error("Expected there to be a cancel error")
} }
@@ -132,11 +124,7 @@ func TestOverwriteRetryCountHeader(t *testing.T) {
}), }),
option.WithHeader("X-Stainless-Retry-Count", "42"), option.WithHeader("X-Stainless-Retry-Count", "42"),
) )
stream := client.Event.ListStreaming(context.Background()) _, err := client.Session.List(context.Background())
for stream.Next() {
// ...
}
err := stream.Err()
if err == nil { if err == nil {
t.Error("Expected there to be a cancel error") t.Error("Expected there to be a cancel error")
} }
@@ -164,11 +152,7 @@ func TestRetryAfterMs(t *testing.T) {
}, },
}), }),
) )
stream := client.Event.ListStreaming(context.Background()) _, err := client.Session.List(context.Background())
for stream.Next() {
// ...
}
err := stream.Err()
if err == nil { if err == nil {
t.Error("Expected there to be a cancel error") t.Error("Expected there to be a cancel error")
} }
@@ -190,11 +174,7 @@ func TestContextCancel(t *testing.T) {
) )
cancelCtx, cancel := context.WithCancel(context.Background()) cancelCtx, cancel := context.WithCancel(context.Background())
cancel() cancel()
stream := client.Event.ListStreaming(cancelCtx) _, err := client.Session.List(cancelCtx)
for stream.Next() {
// ...
}
err := stream.Err()
if err == nil { if err == nil {
t.Error("Expected there to be a cancel error") t.Error("Expected there to be a cancel error")
} }
@@ -213,11 +193,7 @@ func TestContextCancelDelay(t *testing.T) {
) )
cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond) cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel() defer cancel()
stream := client.Event.ListStreaming(cancelCtx) _, err := client.Session.List(cancelCtx)
for stream.Next() {
// ...
}
err := stream.Err()
if err == nil { if err == nil {
t.Error("expected there to be a cancel error") t.Error("expected there to be a cancel error")
} }
@@ -242,11 +218,7 @@ func TestContextDeadline(t *testing.T) {
}, },
}), }),
) )
stream := client.Event.ListStreaming(deadlineCtx) _, err := client.Session.List(deadlineCtx)
for stream.Next() {
// ...
}
err := stream.Err()
if err == nil { if err == nil {
t.Error("expected there to be a deadline error") t.Error("expected there to be a deadline error")
} }
+28 -3
View File
@@ -51,7 +51,8 @@ func (r *ConfigService) Providers(ctx context.Context, opts ...option.RequestOpt
type Config struct { type Config struct {
// JSON schema reference for configuration validation // JSON schema reference for configuration validation
Schema string `json:"$schema"` Schema string `json:"$schema"`
// Share newly created sessions automatically // @deprecated Use 'share' field instead. Share newly created sessions
// automatically
Autoshare bool `json:"autoshare"` Autoshare bool `json:"autoshare"`
// Automatically update to the latest version // Automatically update to the latest version
Autoupdate bool `json:"autoupdate"` Autoupdate bool `json:"autoupdate"`
@@ -71,9 +72,14 @@ type Config struct {
Model string `json:"model"` Model string `json:"model"`
// Custom provider configurations and model overrides // Custom provider configurations and model overrides
Provider map[string]ConfigProvider `json:"provider"` Provider map[string]ConfigProvider `json:"provider"`
// Control sharing behavior: 'auto' enables automatic sharing, 'disabled' disables
// all sharing
Share ConfigShare `json:"share"`
// Theme name to use for the interface // Theme name to use for the interface
Theme string `json:"theme"` Theme string `json:"theme"`
JSON configJSON `json:"-"` // Custom username to display in conversations instead of system username
Username string `json:"username"`
JSON configJSON `json:"-"`
} }
// configJSON contains the JSON metadata for the struct [Config] // configJSON contains the JSON metadata for the struct [Config]
@@ -90,7 +96,9 @@ type configJSON struct {
Mode apijson.Field Mode apijson.Field
Model apijson.Field Model apijson.Field
Provider apijson.Field Provider apijson.Field
Share apijson.Field
Theme apijson.Field Theme apijson.Field
Username apijson.Field
raw string raw string
ExtraFields map[string]apijson.Field ExtraFields map[string]apijson.Field
} }
@@ -470,6 +478,23 @@ func (r configProviderModelsLimitJSON) RawJSON() string {
return r.raw return r.raw
} }
// Control sharing behavior: 'auto' enables automatic sharing, 'disabled' disables
// all sharing
type ConfigShare string
const (
ConfigShareAuto ConfigShare = "auto"
ConfigShareDisabled ConfigShare = "disabled"
)
func (r ConfigShare) IsKnown() bool {
switch r {
case ConfigShareAuto, ConfigShareDisabled:
return true
}
return false
}
type Keybinds struct { type Keybinds struct {
// Exit the application // Exit the application
AppExit string `json:"app_exit,required"` AppExit string `json:"app_exit,required"`
+56 -4
View File
@@ -605,6 +605,7 @@ type Part struct {
Cost float64 `json:"cost"` Cost float64 `json:"cost"`
Filename string `json:"filename"` Filename string `json:"filename"`
Mime string `json:"mime"` Mime string `json:"mime"`
Snapshot string `json:"snapshot"`
// This field can have the runtime type of [ToolPartState]. // This field can have the runtime type of [ToolPartState].
State interface{} `json:"state"` State interface{} `json:"state"`
Synthetic bool `json:"synthetic"` Synthetic bool `json:"synthetic"`
@@ -629,6 +630,7 @@ type partJSON struct {
Cost apijson.Field Cost apijson.Field
Filename apijson.Field Filename apijson.Field
Mime apijson.Field Mime apijson.Field
Snapshot apijson.Field
State apijson.Field State apijson.Field
Synthetic apijson.Field Synthetic apijson.Field
Text apijson.Field Text apijson.Field
@@ -657,13 +659,13 @@ func (r *Part) UnmarshalJSON(data []byte) (err error) {
// for more type safety. // for more type safety.
// //
// Possible runtime types of the union are [TextPart], [FilePart], [ToolPart], // Possible runtime types of the union are [TextPart], [FilePart], [ToolPart],
// [StepStartPart], [StepFinishPart]. // [StepStartPart], [StepFinishPart], [SnapshotPart].
func (r Part) AsUnion() PartUnion { func (r Part) AsUnion() PartUnion {
return r.union return r.union
} }
// Union satisfied by [TextPart], [FilePart], [ToolPart], [StepStartPart] or // Union satisfied by [TextPart], [FilePart], [ToolPart], [StepStartPart],
// [StepFinishPart]. // [StepFinishPart] or [SnapshotPart].
type PartUnion interface { type PartUnion interface {
implementsPart() implementsPart()
} }
@@ -697,6 +699,11 @@ func init() {
Type: reflect.TypeOf(StepFinishPart{}), Type: reflect.TypeOf(StepFinishPart{}),
DiscriminatorValue: "step-finish", DiscriminatorValue: "step-finish",
}, },
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(SnapshotPart{}),
DiscriminatorValue: "snapshot",
},
) )
} }
@@ -708,11 +715,12 @@ const (
PartTypeTool PartType = "tool" PartTypeTool PartType = "tool"
PartTypeStepStart PartType = "step-start" PartTypeStepStart PartType = "step-start"
PartTypeStepFinish PartType = "step-finish" PartTypeStepFinish PartType = "step-finish"
PartTypeSnapshot PartType = "snapshot"
) )
func (r PartType) IsKnown() bool { func (r PartType) IsKnown() bool {
switch r { switch r {
case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish: case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot:
return true return true
} }
return false return false
@@ -816,6 +824,50 @@ func (r sessionShareJSON) RawJSON() string {
return r.raw return r.raw
} }
type SnapshotPart struct {
ID string `json:"id,required"`
MessageID string `json:"messageID,required"`
SessionID string `json:"sessionID,required"`
Snapshot string `json:"snapshot,required"`
Type SnapshotPartType `json:"type,required"`
JSON snapshotPartJSON `json:"-"`
}
// snapshotPartJSON contains the JSON metadata for the struct [SnapshotPart]
type snapshotPartJSON struct {
ID apijson.Field
MessageID apijson.Field
SessionID apijson.Field
Snapshot apijson.Field
Type apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *SnapshotPart) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r snapshotPartJSON) RawJSON() string {
return r.raw
}
func (r SnapshotPart) implementsPart() {}
type SnapshotPartType string
const (
SnapshotPartTypeSnapshot SnapshotPartType = "snapshot"
)
func (r SnapshotPartType) IsKnown() bool {
switch r {
case SnapshotPartTypeSnapshot:
return true
}
return false
}
type StepFinishPart struct { type StepFinishPart struct {
ID string `json:"id,required"` ID string `json:"id,required"`
Cost float64 `json:"cost,required"` Cost float64 `json:"cost,required"`
+2 -5
View File
@@ -23,13 +23,10 @@ func TestUsage(t *testing.T) {
client := opencode.NewClient( client := opencode.NewClient(
option.WithBaseURL(baseURL), option.WithBaseURL(baseURL),
) )
stream := client.Event.ListStreaming(context.TODO()) sessions, err := client.Session.List(context.TODO())
for stream.Next() {
t.Logf("%+v\n", stream.Current())
}
err := stream.Err()
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
} }
t.Logf("%+v\n", sessions)
} }
+1
View File
@@ -33,6 +33,7 @@ export default defineConfig({
solidJs(), solidJs(),
starlight({ starlight({
title: "opencode", title: "opencode",
lastUpdated: true,
expressiveCode: { themes: ["github-light", "github-dark"] }, expressiveCode: { themes: ["github-light", "github-dark"] },
social: [ social: [
{ icon: "github", label: "GitHub", href: config.github }, { icon: "github", label: "GitHub", href: config.github },
Binary file not shown.

Before

Width:  |  Height:  |  Size: 540 KiB

After

Width:  |  Height:  |  Size: 592 KiB

+10 -1
View File
@@ -340,6 +340,7 @@ export default function Share(props: {
const filteredParts = createMemo(() => const filteredParts = createMemo(() =>
msg.parts.filter((x, index) => { msg.parts.filter((x, index) => {
if (x.type === "step-start" && index > 0) return false if (x.type === "step-start" && index > 0) return false
if (x.type === "snapshot") return false
if (x.type === "step-finish") return false if (x.type === "step-finish") return false
if (x.type === "text" && x.synthetic === true) return false if (x.type === "text" && x.synthetic === true) return false
if (x.type === "tool" && x.tool === "todoread") return false if (x.type === "tool" && x.tool === "todoread") return false
@@ -496,7 +497,15 @@ export function fromV1(v1: Message.Info): MessageWithParts {
cost: v1.metadata.assistant!.cost, cost: v1.metadata.assistant!.cost,
path: v1.metadata.assistant!.path, path: v1.metadata.assistant!.path,
summary: v1.metadata.assistant!.summary, summary: v1.metadata.assistant!.summary,
tokens: v1.metadata.assistant!.tokens, tokens: v1.metadata.assistant!.tokens ?? {
input: 0,
output: 0,
cache: {
read: 0,
write: 0,
},
reasoning: 0,
},
modelID: v1.metadata.assistant!.modelID, modelID: v1.metadata.assistant!.modelID,
providerID: v1.metadata.assistant!.providerID, providerID: v1.metadata.assistant!.providerID,
system: v1.metadata.assistant!.system, system: v1.metadata.assistant!.system,
@@ -58,3 +58,11 @@ export function IconMeta(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
</svg> </svg>
) )
} }
// https://icones.js.org/collection/ri?s=robot&icon=ri:robot-2-line
export function IconRobot(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M13.5 2c0 .444-.193.843-.5 1.118V5h5a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3h5V3.118A1.5 1.5 0 1 1 13.5 2M6 7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1zm-4 3H0v6h2zm20 0h2v6h-2zM9 14.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m6 0a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3" /></svg>
)
}
@@ -1,4 +1,4 @@
import { codeToHtml } from "shiki" import { codeToHtml, bundledLanguages } from "shiki"
import { createResource, Suspense } from "solid-js" import { createResource, Suspense } from "solid-js"
import { transformerNotationDiff } from "@shikijs/transformers" import { transformerNotationDiff } from "@shikijs/transformers"
import style from "./content-code.module.css" import style from "./content-code.module.css"
@@ -15,7 +15,7 @@ export function ContentCode(props: Props) {
// TODO: For testing delays // TODO: For testing delays
// await new Promise((resolve) => setTimeout(resolve, 3000)) // await new Promise((resolve) => setTimeout(resolve, 3000))
return (await codeToHtml(code || "", { return (await codeToHtml(code || "", {
lang: lang || "text", lang: lang && lang in bundledLanguages ? lang : "text",
themes: { themes: {
light: "github-light", light: "github-light",
dark: "github-dark", dark: "github-dark",
@@ -23,6 +23,20 @@
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
align-items: stretch; align-items: stretch;
&:first-child {
[data-slot="before"],
[data-slot="after"] {
padding-top: 0.25rem;
}
}
&:last-child {
[data-slot="before"],
[data-slot="after"] {
padding-bottom: 0.25rem;
}
}
[data-slot="before"], [data-slot="before"],
[data-slot="after"] { [data-slot="after"] {
position: relative; position: relative;
@@ -75,46 +89,16 @@
} }
} }
/* .diff > .row:first-child [data-section="cell"]:first-child { */
/* padding-top: 0.5rem; */
/* } */
/**/
/* .diff > .row:last-child [data-section="cell"]:last-child { */
/* padding-bottom: 0.5rem; */
/* } */
/**/
/* [data-section="cell"] { */
/* position: relative; */
/* flex: 1; */
/* display: flex; */
/* flex-direction: column; */
/**/
/* width: 100%; */
/* padding: 0.1875rem 0.5rem 0.1875rem 2.2ch; */
/* margin: 0; */
/**/
/* &[data-display-mobile="true"] { */
/* display: none; */
/* } */
/**/
/* pre { */
/* --shiki-dark-bg: var(--sl-color-bg-surface) !important; */
/* background-color: var(--sl-color-bg-surface) !important; */
/**/
/* white-space: pre-wrap; */
/* word-break: break-word; */
/**/
/* code > span:empty::before { */
/* content: "\00a0"; */
/* white-space: pre; */
/* display: inline-block; */
/* width: 0; */
/* } */
/* } */
/* } */
[data-component="mobile"] { [data-component="mobile"] {
& > [data-component="diff-block"]:first-child > div {
padding-top: 0.25rem;
}
& > [data-component="diff-block"]:last-child > div {
padding-bottom: 0.25rem;
}
& > [data-component="diff-block"] > div { & > [data-component="diff-block"] > div {
padding: 0 1rem 0 2.2ch; padding: 0 1rem 0 2.2ch;
@@ -1,5 +1,5 @@
import { type Component, createMemo } from "solid-js"
import { parsePatch } from "diff" import { parsePatch } from "diff"
import { createMemo } from "solid-js"
import { ContentCode } from "./content-code" import { ContentCode } from "./content-code"
import styles from "./content-diff.module.css" import styles from "./content-diff.module.css"
@@ -90,8 +90,8 @@ export function ContentDiff(props: Props) {
i++ i++
} else if (prefix === " ") { } else if (prefix === " ") {
diffRows.push({ diffRows.push({
left: content, left: content === "" ? " " : content,
right: content, right: content === "" ? " " : content,
type: "unchanged", type: "unchanged",
}) })
i++ i++
@@ -1,21 +1,13 @@
.root { .root {
border: 1px solid var(--sl-color-blue-high);
padding: 0.5rem calc(0.5rem + 3px);
border-radius: 0.25rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 1rem; gap: 1rem;
align-self: flex-start;
&[data-highlight="true"] {
background-color: var(--sl-color-blue-low);
}
[data-slot="expand-button"] { [data-slot="expand-button"] {
flex: 0 0 auto; flex: 0 0 auto;
padding: 2px 0; padding: 2px 0;
font-size: 0.75rem; font-size: 0.857em;
} }
[data-slot="markdown"] { [data-slot="markdown"] {
@@ -29,7 +21,7 @@
display: block; display: block;
} }
font-size: 0.875rem; font-size: 1em;
line-height: 1.5; line-height: 1.5;
p, p,
@@ -61,7 +53,7 @@
h4, h4,
h5, h5,
h6 { h6 {
font-size: 0.875rem; font-size: 1em;
font-weight: 600; font-weight: 600;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@@ -75,7 +67,7 @@
background-color: var(--sl-color-bg-surface) !important; background-color: var(--sl-color-bg-surface) !important;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
line-height: 1.6; line-height: 1.6;
font-size: 0.75rem; font-size: 0.857em;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
@@ -1,10 +1,11 @@
import style from "./content-markdown.module.css"
import { createResource, createSignal } from "solid-js"
import { createOverflow } from "./common"
import { transformerNotationDiff } from "@shikijs/transformers"
import { marked } from "marked" import { marked } from "marked"
import markedShiki from "marked-shiki"
import { codeToHtml } from "shiki" import { codeToHtml } from "shiki"
import markedShiki from "marked-shiki"
import { createOverflow } from "./common"
import { CopyButton } from "./copy-button"
import { createResource, createSignal } from "solid-js"
import { transformerNotationDiff } from "@shikijs/transformers"
import style from "./content-markdown.module.css"
const markedWithShiki = marked.use( const markedWithShiki = marked.use(
markedShiki({ markedShiki({
@@ -54,6 +55,7 @@ export function ContentMarkdown(props: Props) {
{expanded() ? "Show less" : "Show more"} {expanded() ? "Show less" : "Show more"}
</button> </button>
)} )}
<CopyButton text={props.text} />
</div> </div>
) )
} }
@@ -2,6 +2,7 @@
color: var(--sl-color-text); color: var(--sl-color-text);
background-color: var(--sl-color-bg-surface); background-color: var(--sl-color-bg-surface);
padding: 0.5rem calc(0.5rem + 3px); padding: 0.5rem calc(0.5rem + 3px);
padding-right: calc(1rem + 18px);
border-radius: 0.25rem; border-radius: 0.25rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -0,0 +1,31 @@
.root {
position: absolute;
opacity: 0;
visibility: hidden;
transition: opacity 0.15s ease;
button {
cursor: pointer;
background: none;
border: none;
padding: 0.125rem;
background-color: var(--sl-color-bg);
color: var(--sl-color-text-secondary);
svg {
display: block;
width: 1rem;
height: 1rem;
}
&[data-copied="true"] {
color: var(--sl-color-green-high);
}
}
}
/* Show copy button when parent is hovered */
*:hover > .root {
opacity: 1;
visibility: visible;
}
@@ -0,0 +1,36 @@
import { createSignal } from "solid-js"
import { IconClipboard, IconCheckCircle } from "../icons"
import styles from "./copy-button.module.css"
interface CopyButtonProps {
text: string
}
export function CopyButton(props: CopyButtonProps) {
const [copied, setCopied] = createSignal(false)
function handleCopyClick() {
if (props.text) {
navigator.clipboard.writeText(props.text)
.catch((err) => console.error("Copy failed", err))
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}
return (
<div data-component="copy-button" class={styles.root}>
<button
type="button"
onClick={handleCopyClick}
data-copied={copied() ? true : undefined}
>
{copied()
? <IconCheckCircle width={16} height={16} />
: <IconClipboard width={16} height={16} />
}
</button>
</div>
)
}
@@ -103,7 +103,7 @@
[data-component="content"] { [data-component="content"] {
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
padding: 0 0 0.375rem; padding: 0 0 1rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
@@ -135,6 +135,20 @@
gap: 1rem; gap: 1rem;
flex-grow: 1; flex-grow: 1;
max-width: var(--md-tool-width); max-width: var(--md-tool-width);
& > [data-component="assistant-text-markdown"] {
align-self: flex-start;
font-size: 0.875rem;
border: 1px solid var(--sl-color-blue-high);
padding: 0.5rem calc(0.5rem + 3px);
border-radius: 0.25rem;
position: relative;
[data-component="copy-button"] {
top: 0.5rem;
right: calc(0.5rem - 1px);
}
}
} }
[data-component="step-start"] { [data-component="step-start"] {
@@ -142,7 +156,6 @@
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 0.375rem; gap: 0.375rem;
padding-bottom: 1rem;
[data-slot="provider"] { [data-slot="provider"] {
line-height: 18px; line-height: 18px;
@@ -237,6 +250,32 @@
&[data-tool="edit"] { &[data-tool="edit"] {
[data-component="tool-result"] { [data-component="tool-result"] {
max-width: var(--lg-tool-width); max-width: var(--lg-tool-width);
align-items: stretch;
width: 100%;
}
}
&[data-tool="task"] {
[data-component="tool-input"] {
font-size: 0.75rem;
line-height: 1.5;
max-width: var(--md-tool-width);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
[data-component="tool-output"] {
max-width: var(--sm-tool-width);
font-size: 0.75rem;
border: 1px solid var(--sl-color-divider);
padding: 0.5rem calc(0.5rem + 3px);
border-radius: 0.25rem;
position: relative;
[data-component="copy-button"] {
top: 0.5rem;
right: calc(0.5rem - 1px);
}
} }
} }
} }
+41 -21
View File
@@ -19,25 +19,25 @@ import {
IconMagnifyingGlass, IconMagnifyingGlass,
IconDocumentMagnifyingGlass, IconDocumentMagnifyingGlass,
} from "../icons" } from "../icons"
import { IconMeta, IconOpenAI, IconGemini, IconAnthropic } from "../icons/custom" import { IconMeta, IconRobot, IconOpenAI, IconGemini, IconAnthropic } from "../icons/custom"
import { formatDuration } from "../share/common"
import { ContentCode } from "./content-code" import { ContentCode } from "./content-code"
import { ContentDiff } from "./content-diff" import { ContentDiff } from "./content-diff"
import { ContentText } from "./content-text" import { ContentText } from "./content-text"
import { ContentError } from "./content-error"
import { ContentMarkdown } from "./content-markdown"
import { ContentBash } from "./content-bash" import { ContentBash } from "./content-bash"
import { ContentError } from "./content-error"
import { formatDuration } from "../share/common"
import { ContentMarkdown } from "./content-markdown"
import type { MessageV2 } from "opencode/session/message-v2" import type { MessageV2 } from "opencode/session/message-v2"
import type { Diagnostic } from "vscode-languageserver-types" import type { Diagnostic } from "vscode-languageserver-types"
import styles from "./part.module.css" import styles from "./part.module.css"
const MIN_DURATION = 2 const MIN_DURATION = 2000
export interface PartProps { export interface PartProps {
index: number index: number
message: MessageV2.Info message: MessageV2.Info
part: MessageV2.AssistantPart | MessageV2.UserPart part: MessageV2.Part
last: boolean last: boolean
} }
@@ -114,7 +114,7 @@ export function Part(props: PartProps) {
<IconGlobeAlt width={18} height={18} /> <IconGlobeAlt width={18} height={18} />
</Match> </Match>
<Match when={props.part.type === "tool" && props.part.tool === "task"}> <Match when={props.part.type === "tool" && props.part.tool === "task"}>
<IconRectangleStack width={18} height={18} /> <IconRobot width={18} height={18} />
</Match> </Match>
<Match when={true}> <Match when={true}>
<IconSparkles width={18} height={18} /> <IconSparkles width={18} height={18} />
@@ -131,12 +131,13 @@ export function Part(props: PartProps) {
{props.message.role === "user" && props.part.type === "text" && ( {props.message.role === "user" && props.part.type === "text" && (
<div data-component="user-text"> <div data-component="user-text">
<ContentText text={props.part.text} expand={props.last} /> <ContentText text={props.part.text} expand={props.last} />
<Spacer />
</div> </div>
)} )}
{props.message.role === "assistant" && props.part.type === "text" && ( {props.message.role === "assistant" && props.part.type === "text" && (
<div data-component="assistant-text"> <div data-component="assistant-text">
<ContentMarkdown expand={props.last} text={props.part.text} /> <div data-component="assistant-text-markdown">
<ContentMarkdown expand={props.last} text={props.part.text} />
</div>
{props.last && props.message.role === "assistant" && props.message.time.completed && ( {props.last && props.message.role === "assistant" && props.message.time.completed && (
<Footer <Footer
title={DateTime.fromMillis(props.message.time.completed).toLocaleString( title={DateTime.fromMillis(props.message.time.completed).toLocaleString(
@@ -146,7 +147,6 @@ export function Part(props: PartProps) {
{DateTime.fromMillis(props.message.time.completed).toLocaleString(DateTime.DATETIME_MED)} {DateTime.fromMillis(props.message.time.completed).toLocaleString(DateTime.DATETIME_MED)}
</Footer> </Footer>
)} )}
<Spacer />
</div> </div>
)} )}
{props.message.role === "user" && props.part.type === "file" && ( {props.message.role === "user" && props.part.type === "file" && (
@@ -245,6 +245,14 @@ export function Part(props: PartProps) {
state={props.part.state} state={props.part.state}
/> />
</Match> </Match>
<Match when={props.part.tool === "task"}>
<TaskTool
id={props.part.id}
tool={props.part.tool}
message={props.message}
state={props.part.state}
/>
</Match>
<Match when={true}> <Match when={true}>
<FallbackTool <FallbackTool
message={props.message} message={props.message}
@@ -256,11 +264,10 @@ export function Part(props: PartProps) {
</Switch> </Switch>
</div> </div>
<ToolFooter <ToolFooter
time={ time={DateTime.fromMillis(props.part.state.time.end)
DateTime.fromMillis(props.part.state.time.start) .diff(DateTime.fromMillis(props.part.state.time.start))
.diff(DateTime.fromMillis(props.part.state.time.end)) .toMillis()}
.toMillis() />
} />
</> </>
)} )}
</div> </div>
@@ -636,12 +643,25 @@ function Footer(props: ParentProps<{ title: string }>) {
} }
function ToolFooter(props: { time: number }) { function ToolFooter(props: { time: number }) {
return props.time > MIN_DURATION ? ( return props.time > MIN_DURATION && <Footer title={`${props.time}ms`}>{formatDuration(props.time)}</Footer>
<Footer title={`${props.time}ms`}> }
{formatDuration(props.time)}
</Footer> function TaskTool(props: ToolProps) {
) : ( return (
<Spacer /> <>
<div data-component="tool-title">
<span data-slot="name">Task</span>
<span data-slot="target">{props.state.input.description}</span>
</div>
<div data-component="tool-input">
&ldquo;{props.state.input.prompt}&rdquo;
</div>
<ResultsButton showCopy="Show output" hideCopy="Hide output">
<div data-component="tool-output">
<ContentMarkdown expand text={props.state.output} />
</div>
</ResultsButton>
</>
) )
} }
+1 -1
View File
@@ -66,7 +66,7 @@ Logs you into a provider and saves them in the credentials file in `~/.local/sha
opencode auth login opencode auth login
``` ```
When opencode starts up it will loads the providers from the credentials file. And if there are any keys defined in your environments or a `.env` file in your project. When opencode starts up it loads the providers from the credentials file. And if there are any keys defined in your environments or a `.env` file in your project.
--- ---
@@ -5,7 +5,7 @@ description: Using the opencode JSON config.
You can configure opencode using a JSON config file. You can configure opencode using a JSON config file.
```json title="opencode config" ```json title="opencode.json"
{ {
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"theme": "opencode", "theme": "opencode",
+26 -6
View File
@@ -27,12 +27,13 @@ You can add custom providers by specifying the npm package for the provider and
{ {
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"provider": { "provider": {
"openrouter": { "moonshot": {
"name": "OpenRouter", "npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "https://api.moonshot.ai/v1"
},
"models": { "models": {
"weirdo/some-weird-model": { "kimi-k2-0711-preview": {}
"name": "Claude 3.5 Sonnet"
}
} }
} }
} }
@@ -41,10 +42,29 @@ You can add custom providers by specifying the npm package for the provider and
--- ---
### Base URL
You can customize the base URL for any provider by setting the `baseURL` option. This is useful when using proxy services or custom endpoints.
```json title="opencode.json" {6-7}
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"anthropic": {
"options": {
"baseURL": "https://api.anthropic.com/v1"
},
}
}
}
```
---
### Local ### Local
You can configure local model like ones served through LM Studio or Ollama. To You can configure local model like ones served through LM Studio or Ollama. To
do so, you'll need to specify a couple of things. do so, you'll need to specify a couple of things.
Here's an example of configuring a local model from LM Studio: Here's an example of configuring a local model from LM Studio:
-13
View File
@@ -1,13 +0,0 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index 92a80377692488c4ba8801ce33e7736ad7055e43..add6281bbecaa1c03d3b48eb99aead4a7a7336b2 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -1593,7 +1593,7 @@ function prepareCallSettings({
return {
maxTokens,
// TODO v5 remove default 0 for temperature
- temperature: temperature != null ? temperature : 0,
+ temperature: temperature,
topP,
topK,
presencePenalty,
+3
View File
@@ -23,4 +23,7 @@ rm -rf packages/tui/sdk
mv opencode-go/ packages/tui/sdk/ mv opencode-go/ packages/tui/sdk/
rm -rf packages/tui/sdk/.git rm -rf packages/tui/sdk/.git
echo "Kicking off production build..."
stl builds create --branch main --wait false
echo "Done!" echo "Done!"
+9
View File
@@ -0,0 +1,9 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
+7 -3
View File
@@ -78,6 +78,7 @@ resources:
keybinds: KeybindsConfig keybinds: KeybindsConfig
mcpLocal: McpLocalConfig mcpLocal: McpLocalConfig
mcpRemote: McpRemoteConfig mcpRemote: McpRemoteConfig
mode: ModeConfig
provider: Provider provider: Provider
model: Model model: Model
methods: methods:
@@ -94,10 +95,9 @@ resources:
toolPart: ToolPart toolPart: ToolPart
stepStartPart: StepStartPart stepStartPart: StepStartPart
stepFinishPart: StepFinishPart stepFinishPart: StepFinishPart
snapshotPart: SnapshotPart
assistantMessage: AssistantMessage assistantMessage: AssistantMessage
assistantMessagePart: AssistantMessagePart
userMessage: UserMessage userMessage: UserMessage
userMessagePart: UserMessagePart
toolStatePending: ToolStatePending toolStatePending: ToolStatePending
toolStateRunning: ToolStateRunning toolStateRunning: ToolStateRunning
toolStateCompleted: ToolStateCompleted toolStateCompleted: ToolStateCompleted
@@ -126,9 +126,13 @@ readme:
example_requests: example_requests:
default: default:
type: request type: request
endpoint: get /event endpoint: get /session
params: {} params: {}
headline: headline:
type: request
endpoint: get /session
params: {}
streaming:
type: request type: request
endpoint: get /event endpoint: get /event
params: {} params: {}