Compare commits

...

42 commits

Author SHA1 Message Date
Jay V
a272b58fe9 docs: intro
Some checks are pending
deploy / deploy (push) Waiting to run
publish / publish (push) Waiting to run
2025-07-07 17:41:46 -04:00
Dax Raad
9948fcf1b6 fix crash when running on new project 2025-07-07 17:39:52 -04:00
Dax Raad
0d50c867ff fix mcp tools corrupting session 2025-07-07 17:05:16 -04:00
Dax Raad
27f7e02f12 run: truncate prompt 2025-07-07 16:41:42 -04:00
Jay V
0f93ecd564 docs: canonical url 2025-07-07 16:37:00 -04:00
Dax Raad
da909d9684 append piped stdin to prompt 2025-07-07 16:33:21 -04:00
Jay V
facd851b11 docs: dynamic domain 2025-07-07 16:31:15 -04:00
Dax Raad
c51de945a5 Add stdin support to run command
Allow piping content to opencode run when no message arguments are provided, enabling standard Unix pipe patterns for better CLI integration.

🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-07-07 16:29:13 -04:00
Jay V
9253a3ca9e docs: debug 2025-07-07 16:26:23 -04:00
Dax Raad
7cfa297a78 wip: model and prompt flags for tui 2025-07-07 16:24:37 -04:00
Jay V
661b74def6 docs: debug info 2025-07-07 16:13:26 -04:00
Dax Raad
b478e5655c fix interrupt 2025-07-07 16:12:47 -04:00
Dax
f884766445
v2 message format and upgrade to ai sdk v5 (#743)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Liang-Shih Lin <liangshihlin@proton.me>
Co-authored-by: Dominik Engelhardt <dominikengelhardt@ymail.com>
Co-authored-by: Jay V <air@live.ca>
Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
2025-07-07 15:53:43 -04:00
Jay V
76b2e4539c docs: discord 2025-07-07 14:44:37 -04:00
Dominik Engelhardt
d87922c0eb
Fix Elixir LSP startup (#726)
Some checks are pending
deploy / deploy (push) Waiting to run
publish / publish (push) Waiting to run
2025-07-06 23:37:46 -04:00
Liang-Shih Lin
2446483df5
fix: Skip opencode upgrade if same version (#720) 2025-07-06 23:36:59 -04:00
GitHub Action
f4c453155d Update download stats 2025-07-06
Some checks are pending
deploy / deploy (push) Waiting to run
publish / publish (push) Waiting to run
2025-07-06 12:03:56 +00:00
Dax Raad
969ad80ed2 fix openrouter caching with anthropic, should be a lot cheaper
Some checks are pending
deploy / deploy (push) Waiting to run
publish / publish (push) Waiting to run
2025-07-05 11:39:54 -04:00
GitHub Action
af064b41d7 Update download stats 2025-07-05
Some checks are pending
deploy / deploy (push) Waiting to run
publish / publish (push) Waiting to run
2025-07-05 12:03:56 +00:00
Dax Raad
ea6bfef21a use full filepath
Some checks are pending
deploy / deploy (push) Waiting to run
publish / publish (push) Waiting to run
2025-07-04 17:58:03 -04:00
Jay V
107363b1d9 docs: fix show more in share page
Some checks failed
deploy / deploy (push) Has been cancelled
2025-07-04 17:57:12 -04:00
Dax Raad
85214d7c59 fix input bar not rendering capital letters 2025-07-04 17:21:51 -04:00
Timo Clasen
997cb2d945
fix(tui): optimistic rendering (#692) 2025-07-04 16:06:57 -05:00
Dax Raad
45b139390c make file attachments work good like 2025-07-04 16:21:26 -04:00
Jay V
994368de15 docs: share fix scrolling again 2025-07-04 13:53:25 -04:00
Jay V
143fd8e076 docs: share improve markdown rendering of ai responses 2025-07-04 13:53:25 -04:00
Dax Raad
06dba28bd6 wip: fix media type 2025-07-04 12:50:52 -04:00
adamdottv
b8d276a049
fix(tui): full paths for attachments 2025-07-04 11:42:22 -05:00
Dax Raad
ee01f01271 file attachments 2025-07-04 12:24:01 -04:00
adamdottv
32d5db4f0a
fix(tui): markdown wrapping off sometimes 2025-07-04 11:16:38 -05:00
adamdottv
f6108b7be8
fix(tui): handle pdf and image @ files 2025-07-04 11:13:09 -05:00
adamdottv
94ef341c9d
feat(tui): render attachments 2025-07-04 10:55:02 -05:00
adamdottv
f9abc7c84f
feat(tui): file attachments 2025-07-04 10:55:02 -05:00
adamdottv
891ed6ebc0
fix(tui): slower startup due to file.status 2025-07-04 10:55:01 -05:00
Dax Raad
163e23a68b removed banned command concept 2025-07-04 11:32:12 -04:00
Vladimir
f13b0af491
docs: Fix invalid json in the mcp example config (#645) 2025-07-04 11:24:13 -04:00
Aiden Cline
4a0be45d3d
chore: document instructions configuration option (#670) 2025-07-04 11:22:45 -04:00
Dax Raad
23788674c8 disable snapshots temporarily
Some checks are pending
deploy / deploy (push) Waiting to run
publish / publish (push) Waiting to run
2025-07-04 08:45:18 -04:00
GitHub Action
121eb24e73 Update download stats 2025-07-04 2025-07-04 12:26:16 +00:00
Dax Raad
571d60182a improve snapshotting speed further
Some checks are pending
deploy / deploy (push) Waiting to run
publish / publish (push) Waiting to run
2025-07-03 21:36:09 -04:00
Jay V
167a9dcaf3 docs: share fix scroll to anchor
Some checks are pending
deploy / deploy (push) Waiting to run
2025-07-03 20:30:21 -04:00
Dax Raad
37327259cb ci: ignore 2025-07-03 20:30:02 -04:00
135 changed files with 5784 additions and 7443 deletions

View file

@ -28,5 +28,5 @@ jobs:
git config --local user.email "action@github.com" git config --local user.email "action@github.com"
git config --local user.name "GitHub Action" git config --local user.name "GitHub Action"
git add STATS.md git add STATS.md
git diff --staged --quiet || git commit -m "Update download stats $(date -I)" git diff --staged --quiet || git commit -m "ignore: update download stats $(date -I)"
git push git push

View file

@ -9,7 +9,7 @@
</p> </p>
<p align="center">AI coding agent, built for the terminal.</p> <p align="center">AI coding agent, built for the terminal.</p>
<p align="center"> <p align="center">
<a href="https://opencode.ai/docs"><img alt="View docs" src="https://img.shields.io/badge/view-docs-blue?style=flat-square" /></a> <a href="https://discord.gg/opencode"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a> <a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a> <a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p> </p>
@ -76,4 +76,4 @@ The other confusingly named repo has no relation to this one. You can [read the
--- ---
**Join our community** [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev) **Join our community** [Discord](https://discord.gg/opencode) | [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)

View file

@ -7,3 +7,6 @@
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) | | 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) | | 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) | | 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |

View file

@ -5,7 +5,7 @@
"name": "opencode", "name": "opencode",
"devDependencies": { "devDependencies": {
"prettier": "3.5.3", "prettier": "3.5.3",
"sst": "3.17.6", "sst": "3.17.8",
}, },
}, },
"packages/function": { "packages/function": {
@ -78,6 +78,7 @@
"lang-map": "0.4.0", "lang-map": "0.4.0",
"luxon": "3.6.1", "luxon": "3.6.1",
"marked": "15.0.12", "marked": "15.0.12",
"marked-shiki": "1.2.0",
"rehype-autolink-headings": "7.1.0", "rehype-autolink-headings": "7.1.0",
"sharp": "0.32.5", "sharp": "0.32.5",
"shiki": "3.4.2", "shiki": "3.4.2",
@ -95,30 +96,22 @@
"sharp", "sharp",
"esbuild", "esbuild",
], ],
"patchedDependencies": {
"ai@4.3.16": "patches/ai@4.3.16.patch",
},
"overrides": {
"zod": "3.24.2",
},
"catalog": { "catalog": {
"@types/node": "22.13.9", "@types/node": "22.13.9",
"ai": "4.3.16", "ai": "5.0.0-beta.7",
"typescript": "5.8.2", "typescript": "5.8.2",
"zod": "3.24.2", "zod": "3.25.49",
}, },
"packages": { "packages": {
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@2.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="], "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@2.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@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/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-beta.3", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.2" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-g49gMSkXy94lYvl5LRh438OR/0JCG6ol0jV+iLot7cy5HLltZlGocEuauETBu4b10mDXOd7XIjTEZoQpYFMYLQ=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], "@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-beta.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Z8SPncMtS3RsoXITmT7NVwrAq6M44dmw0DoUOYJqNNtCu8iMWuxB8Nxsoqpa0uEEy9R1V1ZThJAXTYgjTUxl3w=="],
"@ai-sdk/react": ["@ai-sdk/react@1.2.12", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g=="], "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-beta.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-H4K+4weOVgWqrDDeAbQWoA4U5mN4WrQPHQFdH7ynQYcnhj/pzctU9Q6mGlR5ESMWxaXxazxlOblSITlXo9bahA=="],
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="],
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
@ -462,12 +455,10 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="], "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
"@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="],
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
@ -492,6 +483,8 @@
"@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], "@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
"@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="],
"@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="], "@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="],
@ -512,7 +505,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@4.3.16", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g=="], "ai": ["ai@5.0.0-beta.7", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-beta.3", "@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-oC4KzUJCQPMB7v9rCqL/rVk2ogZvI6lYiXfKjzPYHwa1zIgy329qqRLmAd3mKEDTTG6By1r0zasQu7FKmG+4gw=="],
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
@ -602,7 +595,7 @@
"buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="], "buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="],
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
@ -730,8 +723,6 @@
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
"diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
"diff3": ["diff3@0.0.3", "", {}, "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g=="], "diff3": ["diff3@0.0.3", "", {}, "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g=="],
"direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="], "direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
@ -800,7 +791,7 @@
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
"eventsource-parser": ["eventsource-parser@3.0.2", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="], "eventsource-parser": ["eventsource-parser@3.0.3", "", {}, "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA=="],
"exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
@ -1022,8 +1013,6 @@
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
@ -1050,6 +1039,8 @@
"marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
"marked-shiki": ["marked-shiki@1.2.0", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-N924hp8veE6Mc91g5/kCNVoTU7TkeJfB2G2XEWb+k1fVA0Bck2T0rVt93d39BlOYH6ohP4Q9BFlPk+UkblhXbg=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="], "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],
@ -1338,8 +1329,6 @@
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
@ -1476,23 +1465,23 @@
"split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="], "split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="],
"sst": ["sst@3.17.6", "", { "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.6", "sst-darwin-x64": "3.17.6", "sst-linux-arm64": "3.17.6", "sst-linux-x64": "3.17.6", "sst-linux-x86": "3.17.6", "sst-win32-arm64": "3.17.6", "sst-win32-x64": "3.17.6", "sst-win32-x86": "3.17.6" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-p+AcqwfYQUdkxeRjCikQoTMviPCBiGoU7M0vcV6GDVmVis8hzhVw4EFfHTafZC+aWfy1Ke2UQi66vZlEVWuEqA=="], "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.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6tb7KlcPR7PTi3ofQv8dX/n6Jf7pNP9VfrnYL4HBWnWrcYaZeJ5MWobILfIJ/y2jHgoqmg9e5C3266Eds0JQyw=="], "sst-darwin-arm64": ["sst-darwin-arm64@3.17.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-50P6YRMnZVItZUfB0+NzqMww2mmm4vB3zhTVtWUtGoXeiw78g1AEnVlmS28gYXPHM1P987jTvR7EON9u9ig/Dg=="],
"sst-darwin-x64": ["sst-darwin-x64@3.17.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-lFakq6/EgTuBSjbl8Kry4pfgAPEIyn6o7ZkyRz3hz5331wUaX88yfjs3tL9JQ8Ey6jBUYxwhP/Q1n7fzIG046g=="], "sst-darwin-x64": ["sst-darwin-x64@3.17.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-P0pnMHCmpkpcsxkWpilmeoD79LkbkoIcv6H0aeM9ArT/71/JBhvqH+HjMHSJCzni/9uR6er+nH5F+qol0UO6Bw=="],
"sst-linux-arm64": ["sst-linux-arm64@3.17.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-SdTxXMbTEdiwOqp37w31kXv97vHqSx3oK9h/76lKg7V9k5JxPJ6JMefPLhoKWwK0Zh6AndY2zo2oRoEv4SIaDw=="], "sst-linux-arm64": ["sst-linux-arm64@3.17.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-vun54YA/UzprCu9p8BC4rMwFU5Cj9xrHAHYLYUp/yq4H0pfmBIiQM62nsfIKizRThe/TkBFy60EEi9myf6raYA=="],
"sst-linux-x64": ["sst-linux-x64@3.17.6", "", { "os": "linux", "cpu": "x64" }, "sha512-qneh7uWDiTUYx8X1Y3h2YVw3SJ0ybBBlRrVybIvCM09JqQ8+qq/XjKXGzA/3/EF0Jr7Ug8cARSn9CwxhdQGN7Q=="], "sst-linux-x64": ["sst-linux-x64@3.17.8", "", { "os": "linux", "cpu": "x64" }, "sha512-HqByCaLE2gEJbM20P1QRd+GqDMAiieuU53FaZA1F+AGxQi+kR82NWjrPqFcMj4dMYg8w/TWXuV+G5+PwoUmpDw=="],
"sst-linux-x86": ["sst-linux-x86@3.17.6", "", { "os": "linux", "cpu": "none" }, "sha512-pU3D5OeqnmfxGqN31DxuwWnc1OayxhkErnITHhZ39D0MTiwbIgCapH26FuLW8B08/uxJWG8djUlOboCRhSBvWA=="], "sst-linux-x86": ["sst-linux-x86@3.17.8", "", { "os": "linux", "cpu": "none" }, "sha512-bCd6QM3MejfSmdvg8I/k+aUJQIZEQJg023qmN78fv00vwlAtfECvY7tjT9E2m3LDp33pXrcRYbFOQzPu+tWFfA=="],
"sst-win32-arm64": ["sst-win32-arm64@3.17.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-Rr3RTYWAsH9sM9CbM/sAZCk7dB1OsSAljjJuuHMvdSAYW3RDpXEza0PBJGxnBID2eOrpswEchzMPL2d8LtL7oA=="], "sst-win32-arm64": ["sst-win32-arm64@3.17.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-pilx0n8gm4aHJae/vNiqIwZkWF3tdwWzD/ON7hkytw+CVSZ0FXtyFW/yO/+2u3Yw0Kj0lSWPnUqYgm/eHPLwQA=="],
"sst-win32-x64": ["sst-win32-x64@3.17.6", "", { "os": "win32", "cpu": "x64" }, "sha512-yZ3roxwI0Wve9PFzdrrF1kfzCmIMFCCoa8qKeXY7LxCJ4QQIqHbCOccLK1Wv/MIU/mcZHWXTQVCLHw77uaa0GQ=="], "sst-win32-x64": ["sst-win32-x64@3.17.8", "", { "os": "win32", "cpu": "x64" }, "sha512-Jb0FVRyiOtESudF1V8ucW65PuHrx/iOHUamIO0JnbujWNHZBTRPB2QHN1dbewgkueYDaCmyS8lvuIImLwYJnzQ=="],
"sst-win32-x86": ["sst-win32-x86@3.17.6", "", { "os": "win32", "cpu": "none" }, "sha512-zV7TJWPJN9PmIXr15iXFSs0tbGsa52oBR3+xiKrUj2qj9XsZe7HBFwskRnHyiFq0durZY9kk9ZtoVlpuUuzr1g=="], "sst-win32-x86": ["sst-win32-x86@3.17.8", "", { "os": "win32", "cpu": "none" }, "sha512-oVmFa/PoElQmfnGJlB0w6rPXiYuldiagO6AbrLMT/6oAnWerLQ8Uhv9tJWfMh3xtPLImQLTjxDo1v0AIzEv9QA=="],
"stacktracey": ["stacktracey@2.1.8", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="], "stacktracey": ["stacktracey@2.1.8", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="],
@ -1524,8 +1513,6 @@
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="],
"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=="],
@ -1534,8 +1521,6 @@
"thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], "thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="],
"throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
"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=="],
@ -1620,8 +1605,6 @@
"url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="], "url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="],
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
@ -1698,7 +1681,7 @@
"youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="], "youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="],
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], "zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="],
"zod-openapi": ["zod-openapi@4.2.4", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="], "zod-openapi": ["zod-openapi@4.2.4", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="],
@ -1710,12 +1693,22 @@
"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-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
"@ai-sdk/amazon-bedrock/aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], "@ai-sdk/amazon-bedrock/aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
"@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
"@ampproject/remapping/@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=="], "@ampproject/remapping/@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=="],
"@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="], "@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="],
"@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/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=="],
@ -1734,6 +1727,8 @@
"@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=="],
"@modelcontextprotocol/sdk/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
"@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=="],
"@openauthjs/openauth/aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], "@openauthjs/openauth/aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
@ -1770,10 +1765,14 @@
"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=="],
"astro/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
"bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"eventsource/eventsource-parser": ["eventsource-parser@3.0.2", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="],
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"get-source/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "get-source/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@ -1786,8 +1785,12 @@
"miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], "miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
"miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
"opencontrol/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="], "opencontrol/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
"opencontrol/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
"opencontrol/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], "opencontrol/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="],
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],

View file

@ -39,6 +39,8 @@ new sst.cloudflare.x.Astro("Web", {
domain, domain,
path: "packages/web", path: "packages/web",
environment: { environment: {
// For astro config
SST_STAGE: $app.stage,
VITE_API_URL: api.url, VITE_API_URL: api.url,
}, },
}) })

View file

@ -1,5 +1,11 @@
{ {
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"mcp": {
"weather": {
"type": "local",
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]
}
},
"experimental": { "experimental": {
"hook": { "hook": {
"file_edited": { "file_edited": {

View file

@ -17,13 +17,13 @@
"catalog": { "catalog": {
"typescript": "5.8.2", "typescript": "5.8.2",
"@types/node": "22.13.9", "@types/node": "22.13.9",
"zod": "3.24.2", "zod": "3.25.49",
"ai": "4.3.16" "ai": "5.0.0-beta.7"
} }
}, },
"devDependencies": { "devDependencies": {
"prettier": "3.5.3", "prettier": "3.5.3",
"sst": "3.17.6" "sst": "3.17.8"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -31,10 +31,8 @@
}, },
"license": "MIT", "license": "MIT",
"prettier": { "prettier": {
"semi": false "semi": false,
}, "printWidth": 120
"overrides": {
"zod": "3.24.2"
}, },
"trustedDependencies": [ "trustedDependencies": [
"esbuild", "esbuild",

View file

@ -38,10 +38,7 @@ export class SyncServer extends DurableObject<Env> {
async publish(key: string, content: any) { async publish(key: string, content: any) {
const sessionID = await this.getSessionID() const sessionID = await this.getSessionID()
if ( if (!key.startsWith(`session/info/${sessionID}`) && !key.startsWith(`session/message/${sessionID}/`))
!key.startsWith(`session/info/${sessionID}`) &&
!key.startsWith(`session/message/${sessionID}/`)
)
return new Response("Error: Invalid key", { status: 400 }) return new Response("Error: Invalid key", { status: 400 })
// store message // store message
@ -184,8 +181,7 @@ export default {
} }
const id = url.searchParams.get("id") const id = url.searchParams.get("id")
console.log("share_poll", id) console.log("share_poll", id)
if (!id) if (!id) return new Response("Error: Share ID is required", { status: 400 })
return new Response("Error: Share ID is required", { status: 400 })
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id)) const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
return stub.fetch(request) return stub.fetch(request)
} }
@ -193,8 +189,7 @@ export default {
if (request.method === "GET" && method === "share_data") { if (request.method === "GET" && method === "share_data") {
const id = url.searchParams.get("id") const id = url.searchParams.get("id")
console.log("share_data", id) console.log("share_data", id)
if (!id) if (!id) return new Response("Error: Share ID is required", { status: 400 })
return new Response("Error: Share ID is required", { status: 400 })
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id)) const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
const data = await stub.getData() const data = await stub.getData()

View file

@ -1,4 +1,3 @@
node_modules
research research
dist dist
gen gen

View file

@ -57,8 +57,7 @@ for (const [os, arch] of targets) {
2, 2,
), ),
) )
if (!dry) if (!dry) await $`cd dist/${name} && bun publish --access public --tag ${npmTag}`
await $`cd dist/${name} && bun publish --access public --tag ${npmTag}`
optionalDependencies[name] = version optionalDependencies[name] = version
} }
@ -82,8 +81,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
2, 2,
), ),
) )
if (!dry) if (!dry) await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}`
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}`
if (!snapshot) { if (!snapshot) {
// Github Release // Github Release
@ -91,15 +89,11 @@ if (!snapshot) {
await $`cd dist/${key}/bin && zip -r ../../${key}.zip *` await $`cd dist/${key}/bin && zip -r ../../${key}.zip *`
} }
const previous = await fetch( const previous = await fetch("https://api.github.com/repos/sst/opencode/releases/latest")
"https://api.github.com/repos/sst/opencode/releases/latest",
)
.then((res) => res.json()) .then((res) => res.json())
.then((data) => data.tag_name) .then((data) => data.tag_name)
const commits = await fetch( const commits = await fetch(`https://api.github.com/repos/sst/opencode/compare/${previous}...HEAD`)
`https://api.github.com/repos/sst/opencode/compare/${previous}...HEAD`,
)
.then((res) => res.json()) .then((res) => res.json())
.then((data) => data.commits || []) .then((data) => data.commits || [])
@ -117,26 +111,13 @@ if (!snapshot) {
}) })
.join("\n") .join("\n")
if (!dry) if (!dry) await $`gh release create v${version} --title "v${version}" --notes ${notes} ./dist/*.zip`
await $`gh release create v${version} --title "v${version}" --notes ${notes} ./dist/*.zip`
// Calculate SHA values // Calculate SHA values
const arm64Sha = const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1` const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
.text() const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
.then((x) => x.trim()) const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const x64Sha =
await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim())
const macX64Sha =
await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim())
const macArm64Sha =
await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim())
// AUR package // AUR package
const pkgbuild = [ const pkgbuild = [
@ -170,9 +151,7 @@ if (!snapshot) {
for (const pkg of ["opencode", "opencode-bin"]) { for (const pkg of ["opencode", "opencode-bin"]) {
await $`rm -rf ./dist/aur-${pkg}` await $`rm -rf ./dist/aur-${pkg}`
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}` await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write( await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild.replace("${pkg}", pkg))
pkgbuild.replace("${pkg}", pkg),
)
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO` await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO` await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"` await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`

View file

@ -45,23 +45,14 @@ export namespace App {
} }
export const provideExisting = ctx.provide export const provideExisting = ctx.provide
export async function provide<T>( export async function provide<T>(input: Input, cb: (app: App.Info) => Promise<T>) {
input: Input,
cb: (app: App.Info) => Promise<T>,
) {
log.info("creating", { log.info("creating", {
cwd: input.cwd, cwd: input.cwd,
}) })
const git = await Filesystem.findUp(".git", input.cwd).then(([x]) => const git = await Filesystem.findUp(".git", input.cwd).then(([x]) => (x ? path.dirname(x) : undefined))
x ? path.dirname(x) : undefined,
)
log.info("git", { git }) log.info("git", { git })
const data = path.join( const data = path.join(Global.Path.data, "project", git ? directory(git) : "global")
Global.Path.data,
"project",
git ? directory(git) : "global",
)
const stateFile = Bun.file(path.join(data, APP_JSON)) const stateFile = Bun.file(path.join(data, APP_JSON))
const state = (await stateFile.json().catch(() => ({}))) as { const state = (await stateFile.json().catch(() => ({}))) as {
initialized: number initialized: number

View file

@ -10,14 +10,8 @@ export namespace AuthAnthropic {
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")
url.searchParams.set( url.searchParams.set("redirect_uri", "https://console.anthropic.com/oauth/code/callback")
"redirect_uri", url.searchParams.set("scope", "org:create_api_key user:profile user:inference")
"https://console.anthropic.com/oauth/code/callback",
)
url.searchParams.set(
"scope",
"org:create_api_key user:profile user:inference",
)
url.searchParams.set("code_challenge", pkce.challenge) url.searchParams.set("code_challenge", pkce.challenge)
url.searchParams.set("code_challenge_method", "S256") url.searchParams.set("code_challenge_method", "S256")
url.searchParams.set("state", pkce.verifier) url.searchParams.set("state", pkce.verifier)
@ -57,9 +51,7 @@ export namespace AuthAnthropic {
const info = await Auth.get("anthropic") const info = await Auth.get("anthropic")
if (!info || info.type !== "oauth") return if (!info || info.type !== "oauth") return
if (info.access && info.expires > Date.now()) return info.access if (info.access && info.expires > Date.now()) return info.access
const response = await fetch( const response = await fetch("https://console.anthropic.com/v1/oauth/token", {
"https://console.anthropic.com/v1/oauth/token",
{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -69,8 +61,7 @@ export namespace AuthAnthropic {
refresh_token: info.refresh, refresh_token: info.refresh,
client_id: CLIENT_ID, client_id: CLIENT_ID,
}), }),
}, })
)
if (!response.ok) return if (!response.ok) return
const json = await response.json() const json = await response.json()
await Auth.set("anthropic", { await Auth.set("anthropic", {

View file

@ -4,9 +4,7 @@ import path from "path"
export const AuthCopilot = lazy(async () => { export const AuthCopilot = lazy(async () => {
const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts")) const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts"))
const response = fetch( const response = fetch("https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts")
"https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts",
)
.then((x) => Bun.write(file, x)) .then((x) => Bun.write(file, x))
.catch(() => {}) .catch(() => {})

View file

@ -122,10 +122,7 @@ export namespace AuthGithubCopilot {
return tokenData.token return tokenData.token
} }
export const DeviceCodeError = NamedError.create( export const DeviceCodeError = NamedError.create("DeviceCodeError", z.object({}))
"DeviceCodeError",
z.object({}),
)
export const TokenExchangeError = NamedError.create( export const TokenExchangeError = NamedError.create(
"TokenExchangeError", "TokenExchangeError",

View file

@ -8,10 +8,7 @@ import { readableStreamToText } from "bun"
export namespace BunProc { export namespace BunProc {
const log = Log.create({ service: "bun" }) const log = Log.create({ service: "bun" })
export async function run( export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
cmd: string[],
options?: Bun.SpawnOptions.OptionsObject<any, any, any>,
) {
log.info("running", { log.info("running", {
cmd: [which(), ...cmd], cmd: [which(), ...cmd],
...options, ...options,
@ -26,9 +23,17 @@ export namespace BunProc {
BUN_BE_BUN: "1", BUN_BE_BUN: "1",
}, },
}) })
const code = await result.exited; const code = await result.exited
const stdout = result.stdout ? typeof result.stdout === "number" ? result.stdout : await readableStreamToText(result.stdout) : undefined const stdout = result.stdout
const stderr = result.stderr ? typeof result.stderr === "number" ? result.stderr : await readableStreamToText(result.stderr) : undefined ? typeof result.stdout === "number"
? result.stdout
: await readableStreamToText(result.stdout)
: undefined
const stderr = result.stderr
? typeof result.stderr === "number"
? result.stderr
: await readableStreamToText(result.stderr)
: undefined
log.info("done", { log.info("done", {
code, code,
stdout, stdout,
@ -61,7 +66,7 @@ export namespace BunProc {
if (parsed.dependencies[pkg] === version) return mod if (parsed.dependencies[pkg] === version) return mod
parsed.dependencies[pkg] = version parsed.dependencies[pkg] = version
await Bun.write(pkgjson, JSON.stringify(parsed, null, 2)) await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
await BunProc.run(["install", "--registry=https://registry.npmjs.org"], { await BunProc.run(["install", "--cwd", Global.Path.cache, "--registry=https://registry.npmjs.org"], {
cwd: Global.Path.cache, cwd: Global.Path.cache,
}).catch((e) => { }).catch((e) => {
throw new InstallFailedError( throw new InstallFailedError(

View file

@ -18,10 +18,7 @@ export namespace Bus {
const registry = new Map<string, EventDefinition>() const registry = new Map<string, EventDefinition>()
export function event<Type extends string, Properties extends ZodType>( export function event<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
type: Type,
properties: Properties,
) {
const result = { const result = {
type, type,
properties, properties,
@ -72,10 +69,7 @@ export namespace Bus {
export function subscribe<Definition extends EventDefinition>( export function subscribe<Definition extends EventDefinition>(
def: Definition, def: Definition,
callback: (event: { callback: (event: { type: Definition["type"]; properties: z.infer<Definition["properties"]> }) => void,
type: Definition["type"]
properties: z.infer<Definition["properties"]>
}) => void,
) { ) {
return raw(def.type, callback) return raw(def.type, callback)
} }

View file

@ -5,10 +5,7 @@ import { Format } from "../format"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { Share } from "../share/share" import { Share } from "../share/share"
export async function bootstrap<T>( export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Promise<T>) {
input: App.Input,
cb: (app: App.Info) => Promise<T>,
) {
return App.provide(input, async (app) => { return App.provide(input, async (app) => {
Share.init() Share.init()
Format.init() Format.init()

View file

@ -15,11 +15,7 @@ export const AuthCommand = cmd({
command: "auth", command: "auth",
describe: "manage credentials", describe: "manage credentials",
builder: (yargs) => builder: (yargs) =>
yargs yargs.command(AuthLoginCommand).command(AuthLogoutCommand).command(AuthListCommand).demandCommand(),
.command(AuthLoginCommand)
.command(AuthLogoutCommand)
.command(AuthListCommand)
.demandCommand(),
async handler() {}, async handler() {},
}) })
@ -31,9 +27,7 @@ export const AuthListCommand = cmd({
UI.empty() UI.empty()
const authPath = path.join(Global.Path.data, "auth.json") const authPath = path.join(Global.Path.data, "auth.json")
const homedir = os.homedir() const homedir = os.homedir()
const displayPath = authPath.startsWith(homedir) const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
? authPath.replace(homedir, "~")
: authPath
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`) prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
const results = await Auth.all().then((x) => Object.entries(x)) const results = await Auth.all().then((x) => Object.entries(x))
const database = await ModelsDev.get() const database = await ModelsDev.get()
@ -114,8 +108,7 @@ export const AuthLoginCommand = cmd({
if (provider === "other") { if (provider === "other") {
provider = await prompts.text({ provider = await prompts.text({
message: "Enter provider id", message: "Enter provider id",
validate: (x) => validate: (x) => (x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only"),
x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only",
}) })
if (prompts.isCancel(provider)) throw new UI.CancelledError() if (prompts.isCancel(provider)) throw new UI.CancelledError()
provider = provider.replace(/^@ai-sdk\//, "") provider = provider.replace(/^@ai-sdk\//, "")
@ -186,17 +179,13 @@ export const AuthLoginCommand = cmd({
await new Promise((resolve) => setTimeout(resolve, 10)) await new Promise((resolve) => setTimeout(resolve, 10))
const deviceInfo = await copilot.authorize() const deviceInfo = await copilot.authorize()
prompts.note( prompts.note(`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`)
`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`,
)
const spinner = prompts.spinner() const spinner = prompts.spinner()
spinner.start("Waiting for authorization...") spinner.start("Waiting for authorization...")
while (true) { while (true) {
await new Promise((resolve) => await new Promise((resolve) => setTimeout(resolve, deviceInfo.interval * 1000))
setTimeout(resolve, deviceInfo.interval * 1000),
)
const response = await copilot.poll(deviceInfo.device) const response = await copilot.poll(deviceInfo.device)
if (response.status === "pending") continue if (response.status === "pending") continue
if (response.status === "success") { if (response.status === "success") {
@ -248,12 +237,7 @@ export const AuthLogoutCommand = cmd({
const providerID = await prompts.select({ const providerID = await prompts.select({
message: "Select provider", message: "Select provider",
options: credentials.map(([key, value]) => ({ options: credentials.map(([key, value]) => ({
label: label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
(database[key]?.name || key) +
UI.Style.TEXT_DIM +
" (" +
value.type +
")",
value: key, value: key,
})), })),
}) })

View file

@ -31,7 +31,6 @@ const FileStatusCommand = cmd({
export const FileCommand = cmd({ export const FileCommand = cmd({
command: "file", command: "file",
builder: (yargs) => builder: (yargs) => yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(),
yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(),
async handler() {}, async handler() {},
}) })

View file

@ -17,9 +17,7 @@ export const DebugCommand = cmd({
command: "wait", command: "wait",
async handler() { async handler() {
await bootstrap({ cwd: process.cwd() }, async () => { await bootstrap({ cwd: process.cwd() }, async () => {
await new Promise((resolve) => await new Promise((resolve) => setTimeout(resolve, 1_000 * 60 * 60 * 24))
setTimeout(resolve, 1_000 * 60 * 60 * 24),
)
}) })
}, },
}) })

View file

@ -5,15 +5,13 @@ import { Log } from "../../../util/log"
export const LSPCommand = cmd({ export const LSPCommand = cmd({
command: "lsp", command: "lsp",
builder: (yargs) => builder: (yargs) => yargs.command(DiagnosticsCommand).command(SymbolsCommand).demandCommand(),
yargs.command(DiagnosticsCommand).command(SymbolsCommand).demandCommand(),
async handler() {}, async handler() {},
}) })
const DiagnosticsCommand = cmd({ const DiagnosticsCommand = cmd({
command: "diagnostics <file>", command: "diagnostics <file>",
builder: (yargs) => builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
yargs.positional("file", { type: "string", demandOption: true }),
async handler(args) { async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => { await bootstrap({ cwd: process.cwd() }, async () => {
await LSP.touchFile(args.file, true) await LSP.touchFile(args.file, true)
@ -24,8 +22,7 @@ const DiagnosticsCommand = cmd({
export const SymbolsCommand = cmd({ export const SymbolsCommand = cmd({
command: "symbols <query>", command: "symbols <query>",
builder: (yargs) => builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }),
yargs.positional("query", { type: "string", demandOption: true }),
async handler(args) { async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => { await bootstrap({ cwd: process.cwd() }, async () => {
await LSP.touchFile("./src/index.ts", true) await LSP.touchFile("./src/index.ts", true)

View file

@ -5,12 +5,7 @@ import { cmd } from "../cmd"
export const RipgrepCommand = cmd({ export const RipgrepCommand = cmd({
command: "rg", command: "rg",
builder: (yargs) => builder: (yargs) => yargs.command(TreeCommand).command(FilesCommand).command(SearchCommand).demandCommand(),
yargs
.command(TreeCommand)
.command(FilesCommand)
.command(SearchCommand)
.demandCommand(),
async handler() {}, async handler() {},
}) })

View file

@ -4,11 +4,7 @@ import { cmd } from "../cmd"
export const SnapshotCommand = cmd({ export const SnapshotCommand = cmd({
command: "snapshot", command: "snapshot",
builder: (yargs) => builder: (yargs) => yargs.command(SnapshotCreateCommand).command(SnapshotRestoreCommand).demandCommand(),
yargs
.command(SnapshotCreateCommand)
.command(SnapshotRestoreCommand)
.demandCommand(),
async handler() {}, async handler() {},
}) })

View file

@ -10,9 +10,6 @@ export const GenerateCommand = {
const dir = "gen" const dir = "gen"
await fs.rmdir(dir, { recursive: true }).catch(() => {}) await fs.rmdir(dir, { recursive: true }).catch(() => {})
await fs.mkdir(dir, { recursive: true }) await fs.mkdir(dir, { recursive: true })
await Bun.write( await Bun.write(path.join(dir, "openapi.json"), JSON.stringify(specs, null, 2))
path.join(dir, "openapi.json"),
JSON.stringify(specs, null, 2),
)
}, },
} satisfies CommandModule } satisfies CommandModule

View file

@ -2,12 +2,12 @@ import type { Argv } from "yargs"
import { Bus } from "../../bus" import { Bus } from "../../bus"
import { Provider } from "../../provider/provider" import { Provider } from "../../provider/provider"
import { Session } from "../../session" import { Session } from "../../session"
import { Message } from "../../session/message"
import { UI } from "../ui" import { UI } from "../ui"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { Flag } from "../../flag/flag" import { Flag } from "../../flag/flag"
import { Config } from "../../config/config" import { Config } from "../../config/config"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { MessageV2 } from "../../session/message-v2"
const TOOL: Record<string, [string, string]> = { const TOOL: Record<string, [string, string]> = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD], todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@ -54,7 +54,10 @@ export const RunCommand = cmd({
}) })
}, },
handler: async (args) => { handler: async (args) => {
const message = args.message.join(" ") let message = args.message.join(" ")
if (!process.stdin.isTTY) message += "\n" + (await Bun.stdin.text())
await bootstrap({ cwd: process.cwd() }, async () => { await bootstrap({ cwd: process.cwd() }, async () => {
const session = await (async () => { const session = await (async () => {
if (args.continue) { if (args.continue) {
@ -78,27 +81,19 @@ export const RunCommand = cmd({
UI.empty() UI.empty()
UI.println(UI.logo()) UI.println(UI.logo())
UI.empty() UI.empty()
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message) const displayMessage = message.length > 300 ? message.slice(0, 300) + "..." : message
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", displayMessage)
UI.empty() UI.empty()
const cfg = await Config.get() const cfg = await Config.get()
if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) { if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
await Session.share(session.id) await Session.share(session.id)
UI.println( UI.println(UI.Style.TEXT_INFO_BOLD + "~ https://opencode.ai/s/" + session.id.slice(-8))
UI.Style.TEXT_INFO_BOLD +
"~ https://opencode.ai/s/" +
session.id.slice(-8),
)
} }
UI.empty() UI.empty()
const { providerID, modelID } = args.model const { providerID, modelID } = args.model ? Provider.parseModel(args.model) : await Provider.defaultModel()
? Provider.parseModel(args.model) UI.println(UI.Style.TEXT_NORMAL_BOLD + "@ ", UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`)
: await Provider.defaultModel()
UI.println(
UI.Style.TEXT_NORMAL_BOLD + "@ ",
UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`,
)
UI.empty() UI.empty()
function printEvent(color: string, type: string, title: string) { function printEvent(color: string, type: string, title: string) {
@ -110,24 +105,13 @@ export const RunCommand = cmd({
) )
} }
Bus.subscribe(Message.Event.PartUpdated, async (evt) => { Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
if (evt.properties.sessionID !== session.id) return if (evt.properties.sessionID !== session.id) return
const part = evt.properties.part const part = evt.properties.part
const message = await Session.getMessage(
evt.properties.sessionID,
evt.properties.messageID,
)
if ( if (part.type === "tool" && part.state.status === "completed") {
part.type === "tool-invocation" && const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD]
part.toolInvocation.state === "result" printEvent(color, tool, part.state.title || "Unknown")
) {
const metadata = message.metadata.tool[part.toolInvocation.toolCallId]
const [tool, color] = TOOL[part.toolInvocation.toolName] ?? [
part.toolInvocation.toolName,
UI.Style.TEXT_INFO_BOLD,
]
printEvent(color, tool, metadata?.title || "Unknown")
} }
if (part.type === "text") { if (part.type === "text") {

View file

@ -38,9 +38,7 @@ export const ServeCommand = cmd({
hostname, hostname,
}) })
console.log( console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
`opencode server listening on http://${server.hostname}:${server.port}`,
)
await new Promise(() => {}) await new Promise(() => {})

View file

@ -15,9 +15,20 @@ export const TuiCommand = cmd({
command: "$0 [project]", command: "$0 [project]",
describe: "start opencode tui", describe: "start opencode tui",
builder: (yargs) => builder: (yargs) =>
yargs.positional("project", { yargs
.positional("project", {
type: "string", type: "string",
describe: "path to start opencode in", describe: "path to start opencode in",
})
.option("model", {
type: "string",
alias: ["m"],
describe: "model to use in the format of provider/model",
})
.option("prompt", {
alias: ["p"],
type: "string",
describe: "prompt to use",
}), }),
handler: async (args) => { handler: async (args) => {
while (true) { while (true) {
@ -40,9 +51,7 @@ export const TuiCommand = cmd({
}) })
let cmd = ["go", "run", "./main.go"] let cmd = ["go", "run", "./main.go"]
let cwd = Bun.fileURLToPath( let cwd = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url))
new URL("../../../../tui/cmd/opencode", import.meta.url),
)
if (Bun.embeddedFiles.length > 0) { if (Bun.embeddedFiles.length > 0) {
const blob = Bun.embeddedFiles[0] as File const blob = Bun.embeddedFiles[0] as File
let binaryName = blob.name let binaryName = blob.name
@ -62,7 +71,11 @@ export const TuiCommand = cmd({
cmd, cmd,
}) })
const proc = Bun.spawn({ const proc = Bun.spawn({
cmd: [...cmd, ...process.argv.slice(2)], cmd: [
...cmd,
...(args.model ? ["--model", args.model] : []),
...(args.prompt ? ["--prompt", args.prompt] : []),
],
cwd, cwd,
stdout: "inherit", stdout: "inherit",
stderr: "inherit", stderr: "inherit",

View file

@ -27,22 +27,26 @@ export const UpgradeCommand = {
const detectedMethod = await Installation.method() const detectedMethod = await Installation.method()
const method = (args.method as Installation.Method) ?? detectedMethod const method = (args.method as Installation.Method) ?? detectedMethod
if (method === "unknown") { if (method === "unknown") {
prompts.log.error( prompts.log.error(`opencode is installed to ${process.execPath} and seems to be managed by a package manager`)
`opencode is installed to ${process.execPath} and seems to be managed by a package manager`,
)
prompts.outro("Done") prompts.outro("Done")
return return
} }
prompts.log.info("Using method: " + method) prompts.log.info("Using method: " + method)
const target = args.target ?? (await Installation.latest()) const target = args.target ?? (await Installation.latest())
if (Installation.VERSION === target) {
prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`)
prompts.outro("Done")
return
}
prompts.log.info(`From ${Installation.VERSION}${target}`) prompts.log.info(`From ${Installation.VERSION}${target}`)
const spinner = prompts.spinner() const spinner = prompts.spinner()
spinner.start("Upgrading...") spinner.start("Upgrading...")
const err = await Installation.upgrade(method, target).catch((err) => err) const err = await Installation.upgrade(method, target).catch((err) => err)
if (err) { if (err) {
spinner.stop("Upgrade failed") spinner.stop("Upgrade failed")
if (err instanceof Installation.UpgradeFailedError) if (err instanceof Installation.UpgradeFailedError) prompts.log.error(err.data.stderr)
prompts.log.error(err.data.stderr)
else if (err instanceof Error) prompts.log.error(err.message) else if (err instanceof Error) prompts.log.error(err.message)
prompts.outro("Done") prompts.outro("Done")
return return

View file

@ -5,14 +5,11 @@ import { UI } from "./ui"
export function FormatError(input: unknown) { export function FormatError(input: unknown) {
if (MCP.Failed.isInstance(input)) if (MCP.Failed.isInstance(input))
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.` return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
if (Config.JsonError.isInstance(input)) if (Config.JsonError.isInstance(input)) return `Config file at ${input.data.path} is not valid JSON`
return `Config file at ${input.data.path} is not valid JSON`
if (Config.InvalidError.isInstance(input)) if (Config.InvalidError.isInstance(input))
return [ return [
`Config file at ${input.data.path} is invalid`, `Config file at ${input.data.path} is invalid`,
...(input.data.issues?.map( ...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []),
(issue) => "↳ " + issue.message + " " + issue.path.join("."),
) ?? []),
].join("\n") ].join("\n")
if (UI.CancelledError.isInstance(input)) return "" if (UI.CancelledError.isInstance(input)) return ""

View file

@ -29,18 +29,12 @@ export namespace Config {
export const McpLocal = z export const McpLocal = z
.object({ .object({
type: z.literal("local").describe("Type of MCP server connection"), type: z.literal("local").describe("Type of MCP server connection"),
command: z command: z.string().array().describe("Command and arguments to run the MCP server"),
.string()
.array()
.describe("Command and arguments to run the MCP server"),
environment: z environment: z
.record(z.string(), z.string()) .record(z.string(), z.string())
.optional() .optional()
.describe("Environment variables to set when running the MCP server"), .describe("Environment variables to set when running the MCP server"),
enabled: z enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
.boolean()
.optional()
.describe("Enable or disable the MCP server on startup"),
}) })
.strict() .strict()
.openapi({ .openapi({
@ -51,10 +45,7 @@ export namespace Config {
.object({ .object({
type: z.literal("remote").describe("Type of MCP server connection"), type: z.literal("remote").describe("Type of MCP server connection"),
url: z.string().describe("URL of the remote MCP server"), url: z.string().describe("URL of the remote MCP server"),
enabled: z enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
.boolean()
.optional()
.describe("Enable or disable the MCP server on startup"),
}) })
.strict() .strict()
.openapi({ .openapi({
@ -66,67 +57,31 @@ export namespace Config {
export const Keybinds = z export const Keybinds = z
.object({ .object({
leader: z leader: z.string().optional().describe("Leader key for keybind combinations"),
.string()
.optional()
.describe("Leader key for keybind combinations"),
help: z.string().optional().describe("Show help dialog"), help: z.string().optional().describe("Show help dialog"),
editor_open: z.string().optional().describe("Open external editor"), editor_open: z.string().optional().describe("Open external editor"),
session_new: z.string().optional().describe("Create a new session"), session_new: z.string().optional().describe("Create a new session"),
session_list: z.string().optional().describe("List all sessions"), session_list: z.string().optional().describe("List all sessions"),
session_share: z.string().optional().describe("Share current session"), session_share: z.string().optional().describe("Share current session"),
session_interrupt: z session_interrupt: z.string().optional().describe("Interrupt current session"),
.string() session_compact: z.string().optional().describe("Toggle compact mode for session"),
.optional()
.describe("Interrupt current session"),
session_compact: z
.string()
.optional()
.describe("Toggle compact mode for session"),
tool_details: z.string().optional().describe("Show tool details"), tool_details: z.string().optional().describe("Show tool details"),
model_list: z.string().optional().describe("List available models"), model_list: z.string().optional().describe("List available models"),
theme_list: z.string().optional().describe("List available themes"), theme_list: z.string().optional().describe("List available themes"),
project_init: z project_init: z.string().optional().describe("Initialize project configuration"),
.string()
.optional()
.describe("Initialize project configuration"),
input_clear: z.string().optional().describe("Clear input field"), input_clear: z.string().optional().describe("Clear input field"),
input_paste: z.string().optional().describe("Paste from clipboard"), input_paste: z.string().optional().describe("Paste from clipboard"),
input_submit: z.string().optional().describe("Submit input"), input_submit: z.string().optional().describe("Submit input"),
input_newline: z.string().optional().describe("Insert newline in input"), input_newline: z.string().optional().describe("Insert newline in input"),
history_previous: z history_previous: z.string().optional().describe("Navigate to previous history item"),
.string() history_next: z.string().optional().describe("Navigate to next history item"),
.optional() messages_page_up: z.string().optional().describe("Scroll messages up by one page"),
.describe("Navigate to previous history item"), messages_page_down: z.string().optional().describe("Scroll messages down by one page"),
history_next: z messages_half_page_up: z.string().optional().describe("Scroll messages up by half page"),
.string() messages_half_page_down: z.string().optional().describe("Scroll messages down by half page"),
.optional() messages_previous: z.string().optional().describe("Navigate to previous message"),
.describe("Navigate to next history item"),
messages_page_up: z
.string()
.optional()
.describe("Scroll messages up by one page"),
messages_page_down: z
.string()
.optional()
.describe("Scroll messages down by one page"),
messages_half_page_up: z
.string()
.optional()
.describe("Scroll messages up by half page"),
messages_half_page_down: z
.string()
.optional()
.describe("Scroll messages down by half page"),
messages_previous: z
.string()
.optional()
.describe("Navigate to previous message"),
messages_next: z.string().optional().describe("Navigate to next message"), messages_next: z.string().optional().describe("Navigate to next message"),
messages_first: z messages_first: z.string().optional().describe("Navigate to first message"),
.string()
.optional()
.describe("Navigate to first message"),
messages_last: z.string().optional().describe("Navigate to last message"), messages_last: z.string().optional().describe("Navigate to last message"),
app_exit: z.string().optional().describe("Exit the application"), app_exit: z.string().optional().describe("Exit the application"),
}) })
@ -136,33 +91,13 @@ export namespace Config {
}) })
export const Info = z export const Info = z
.object({ .object({
$schema: z $schema: z.string().optional().describe("JSON schema reference for configuration validation"),
.string() theme: z.string().optional().describe("Theme name to use for the interface"),
.optional()
.describe("JSON schema reference for configuration validation"),
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"),
autoshare: z autoshare: z.boolean().optional().describe("Share newly created sessions automatically"),
.boolean() autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
.optional() disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
.describe("Share newly created sessions automatically"), model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
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"),
model: z
.string()
.describe(
"Model to use in the format of provider/model, eg anthropic/claude-2",
)
.optional(),
provider: z provider: z
.record( .record(
ModelsDev.Provider.partial().extend({ ModelsDev.Provider.partial().extend({
@ -172,14 +107,8 @@ export namespace Config {
) )
.optional() .optional()
.describe("Custom provider configurations and model overrides"), .describe("Custom provider configurations and model overrides"),
mcp: z mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
.record(z.string(), Mcp) instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
.optional()
.describe("MCP (Model Context Protocol) server configurations"),
instructions: z
.array(z.string())
.optional()
.describe("Additional instruction files or patterns to include"),
experimental: z experimental: z
.object({ .object({
hook: z hook: z
@ -227,10 +156,7 @@ export namespace Config {
if (provider && model) result.model = `${provider}/${model}` if (provider && model) result.model = `${provider}/${model}`
result["$schema"] = "https://opencode.ai/config.json" result["$schema"] = "https://opencode.ai/config.json"
result = mergeDeep(result, rest) result = mergeDeep(result, rest)
await Bun.write( await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
path.join(Global.Path.config, "config.json"),
JSON.stringify(result, null, 2),
)
await fs.unlink(path.join(Global.Path.config, "config")) await fs.unlink(path.join(Global.Path.config, "config"))
}) })
.catch(() => {}) .catch(() => {})

View file

@ -22,9 +22,7 @@ export namespace ConfigHooks {
command: item.command, command: item.command,
}) })
Bun.spawn({ Bun.spawn({
cmd: item.command.map((x) => cmd: item.command.map((x) => x.replace("$FILE", payload.properties.file)),
x.replace("$FILE", payload.properties.file),
),
env: item.environment, env: item.environment,
cwd: app.path.cwd, cwd: app.path.cwd,
stdout: "ignore", stdout: "ignore",

View file

@ -45,10 +45,7 @@ export namespace Fzf {
log.info("found", { filepath }) log.info("found", { filepath })
return { filepath } return { filepath }
} }
filepath = path.join( filepath = path.join(Global.Path.bin, "fzf" + (process.platform === "win32" ? ".exe" : ""))
Global.Path.bin,
"fzf" + (process.platform === "win32" ? ".exe" : ""),
)
const file = Bun.file(filepath) const file = Bun.file(filepath)
if (!(await file.exists())) { if (!(await file.exists())) {
@ -56,18 +53,15 @@ export namespace Fzf {
const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64" const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64"
const config = PLATFORM[process.platform as keyof typeof PLATFORM] const config = PLATFORM[process.platform as keyof typeof PLATFORM]
if (!config) if (!config) throw new UnsupportedPlatformError({ platform: process.platform })
throw new UnsupportedPlatformError({ platform: process.platform })
const version = VERSION const version = VERSION
const platformName = const platformName = process.platform === "win32" ? "windows" : process.platform
process.platform === "win32" ? "windows" : process.platform
const filename = `fzf-${version}-${platformName}_${arch}.${config.extension}` const filename = `fzf-${version}-${platformName}_${arch}.${config.extension}`
const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}` const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}`
const response = await fetch(url) const response = await fetch(url)
if (!response.ok) if (!response.ok) throw new DownloadFailedError({ url, status: response.status })
throw new DownloadFailedError({ url, status: response.status })
const buffer = await response.arrayBuffer() const buffer = await response.arrayBuffer()
const archivePath = path.join(Global.Path.bin, filename) const archivePath = path.join(Global.Path.bin, filename)
@ -86,14 +80,11 @@ export namespace Fzf {
}) })
} }
if (config.extension === "zip") { if (config.extension === "zip") {
const proc = Bun.spawn( const proc = Bun.spawn(["unzip", "-j", archivePath, "fzf.exe", "-d", Global.Path.bin], {
["unzip", "-j", archivePath, "fzf.exe", "-d", Global.Path.bin],
{
cwd: Global.Path.bin, cwd: Global.Path.bin,
stderr: "pipe", stderr: "pipe",
stdout: "ignore", stdout: "ignore",
}, })
)
await proc.exited await proc.exited
if (proc.exitCode !== 0) if (proc.exitCode !== 0)
throw new ExtractionFailedError({ throw new ExtractionFailedError({

View file

@ -24,11 +24,7 @@ export namespace File {
const app = App.info() const app = App.info()
if (!app.git) return [] if (!app.git) return []
const diffOutput = await $`git diff --numstat HEAD` const diffOutput = await $`git diff --numstat HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
.cwd(app.path.cwd)
.quiet()
.nothrow()
.text()
const changedFiles = [] const changedFiles = []
@ -45,19 +41,13 @@ export namespace File {
} }
} }
const untrackedOutput = await $`git ls-files --others --exclude-standard` const untrackedOutput = await $`git ls-files --others --exclude-standard`.cwd(app.path.cwd).quiet().nothrow().text()
.cwd(app.path.cwd)
.quiet()
.nothrow()
.text()
if (untrackedOutput.trim()) { if (untrackedOutput.trim()) {
const untrackedFiles = untrackedOutput.trim().split("\n") const untrackedFiles = untrackedOutput.trim().split("\n")
for (const filepath of untrackedFiles) { for (const filepath of untrackedFiles) {
try { try {
const content = await Bun.file( const content = await Bun.file(path.join(app.path.root, filepath)).text()
path.join(app.path.root, filepath),
).text()
const lines = content.split("\n").length const lines = content.split("\n").length
changedFiles.push({ changedFiles.push({
file: filepath, file: filepath,
@ -72,11 +62,7 @@ export namespace File {
} }
// Get deleted files // Get deleted files
const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD` const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
.cwd(app.path.cwd)
.quiet()
.nothrow()
.text()
if (deletedOutput.trim()) { if (deletedOutput.trim()) {
const deletedFiles = deletedOutput.trim().split("\n") const deletedFiles = deletedOutput.trim().split("\n")
@ -112,11 +98,7 @@ export namespace File {
filepath: rel, filepath: rel,
}) })
if (diff !== "unmodified") { if (diff !== "unmodified") {
const original = await $`git show HEAD:${rel}` const original = await $`git show HEAD:${rel}`.cwd(app.path.root).quiet().nothrow().text()
.cwd(app.path.root)
.quiet()
.nothrow()
.text()
const patch = createPatch(file, original, content, "old", "new", { const patch = createPatch(file, original, content, "old", "new", {
context: Infinity, context: Infinity,
}) })

View file

@ -122,15 +122,11 @@ export namespace Ripgrep {
const state = lazy(async () => { const state = lazy(async () => {
let filepath = Bun.which("rg") let filepath = Bun.which("rg")
if (filepath) return { filepath } if (filepath) return { filepath }
filepath = path.join( filepath = path.join(Global.Path.bin, "rg" + (process.platform === "win32" ? ".exe" : ""))
Global.Path.bin,
"rg" + (process.platform === "win32" ? ".exe" : ""),
)
const file = Bun.file(filepath) const file = Bun.file(filepath)
if (!(await file.exists())) { if (!(await file.exists())) {
const platformKey = const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM
`${process.arch}-${process.platform}` as keyof typeof PLATFORM
const config = PLATFORM[platformKey] const config = PLATFORM[platformKey]
if (!config) throw new UnsupportedPlatformError({ platform: platformKey }) if (!config) throw new UnsupportedPlatformError({ platform: platformKey })
@ -139,8 +135,7 @@ export namespace Ripgrep {
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}` const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}`
const response = await fetch(url) const response = await fetch(url)
if (!response.ok) if (!response.ok) throw new DownloadFailedError({ url, status: response.status })
throw new DownloadFailedError({ url, status: response.status })
const buffer = await response.arrayBuffer() const buffer = await response.arrayBuffer()
const archivePath = path.join(Global.Path.bin, filename) const archivePath = path.join(Global.Path.bin, filename)
@ -164,14 +159,11 @@ export namespace Ripgrep {
}) })
} }
if (config.extension === "zip") { if (config.extension === "zip") {
const proc = Bun.spawn( const proc = Bun.spawn(["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin], {
["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin],
{
cwd: Global.Path.bin, cwd: Global.Path.bin,
stderr: "pipe", stderr: "pipe",
stdout: "ignore", stdout: "ignore",
}, })
)
await proc.exited await proc.exited
if (proc.exitCode !== 0) if (proc.exitCode !== 0)
throw new ExtractionFailedError({ throw new ExtractionFailedError({
@ -193,17 +185,11 @@ export namespace Ripgrep {
return filepath return filepath
} }
export async function files(input: { export async function files(input: { cwd: string; query?: string; glob?: string; limit?: number }) {
cwd: string
query?: string
glob?: string
limit?: number
}) {
const commands = [ const commands = [
`${await filepath()} --files --hidden --glob='!.git/*' ${input.glob ? `--glob='${input.glob}'` : ``}`, `${await filepath()} --files --hidden --glob='!.git/*' ${input.glob ? `--glob='${input.glob}'` : ``}`,
] ]
if (input.query) if (input.query) commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
if (input.limit) commands.push(`head -n ${input.limit}`) if (input.limit) commands.push(`head -n ${input.limit}`)
const joined = commands.join(" | ") const joined = commands.join(" | ")
const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text() const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
@ -310,18 +296,8 @@ export namespace Ripgrep {
return lines.join("\n") return lines.join("\n")
} }
export async function search(input: { export async function search(input: { cwd: string; pattern: string; glob?: string[]; limit?: number }) {
cwd: string const args = [`${await filepath()}`, "--json", "--hidden", "--glob='!.git/*'"]
pattern: string
glob?: string[]
limit?: number
}) {
const args = [
`${await filepath()}`,
"--json",
"--hidden",
"--glob='!.git/*'",
]
if (input.glob) { if (input.glob) {
for (const g of input.glob) { for (const g of input.glob) {

View file

@ -1,6 +1,8 @@
import { App } from "../app/app" import { App } from "../app/app"
import { Log } from "../util/log"
export namespace FileTime { export namespace FileTime {
const log = Log.create({ service: "file.time" })
export const state = App.state("tool.filetimes", () => { export const state = App.state("tool.filetimes", () => {
const read: { const read: {
[sessionID: string]: { [sessionID: string]: {
@ -13,6 +15,7 @@ export namespace FileTime {
}) })
export function read(sessionID: string, file: string) { export function read(sessionID: string, file: string) {
log.info("read", { sessionID, file })
const { read } = state() const { read } = state()
read[sessionID] = read[sessionID] || {} read[sessionID] = read[sessionID] || {}
read[sessionID][file] = new Date() read[sessionID][file] = new Date()
@ -24,10 +27,7 @@ export namespace FileTime {
export async function assert(sessionID: string, filepath: string) { export async function assert(sessionID: string, filepath: string) {
const time = get(sessionID, filepath) const time = get(sessionID, filepath)
if (!time) if (!time) throw new Error(`You must read the file ${filepath} before overwriting it. Use the Read tool first`)
throw new Error(
`You must read the file ${filepath} before overwriting it. Use the Read tool first`,
)
const stats = await Bun.file(filepath).stat() const stats = await Bun.file(filepath).stat()
if (stats.mtime.getTime() > time.getTime()) { if (stats.mtime.getTime() > time.getTime()) {
throw new Error( throw new Error(

View file

@ -21,11 +21,9 @@ export namespace FileWatcher {
"file.watcher", "file.watcher",
() => { () => {
const app = App.use() const app = App.use()
if (!app.info.git) return {}
try { try {
const watcher = fs.watch( const watcher = fs.watch(app.info.path.cwd, { recursive: true }, (event, file) => {
app.info.path.cwd,
{ recursive: true },
(event, file) => {
log.info("change", { file, event }) log.info("change", { file, event })
if (!file) return if (!file) return
// for some reason async local storage is lost here // for some reason async local storage is lost here
@ -36,8 +34,7 @@ export namespace FileWatcher {
event, event,
}) })
}) })
}, })
)
return { watcher } return { watcher }
} catch { } catch {
return {} return {}

View file

@ -94,21 +94,7 @@ export const zig: Info = {
export const clang: Info = { export const clang: Info = {
name: "clang-format", name: "clang-format",
command: ["clang-format", "-i", "$FILE"], command: ["clang-format", "-i", "$FILE"],
extensions: [ extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
".c",
".cc",
".cpp",
".cxx",
".c++",
".h",
".hh",
".hpp",
".hxx",
".h++",
".ino",
".C",
".H",
],
async enabled() { async enabled() {
return Bun.which("clang-format") !== null return Bun.which("clang-format") !== null
}, },

View file

@ -26,11 +26,7 @@ export namespace Identifier {
return generateID(prefix, true, given) return generateID(prefix, true, given)
} }
function generateID( function generateID(prefix: keyof typeof prefixes, descending: boolean, given?: string): string {
prefix: keyof typeof prefixes,
descending: boolean,
given?: string,
): string {
if (!given) { if (!given) {
return generateNewID(prefix, descending) return generateNewID(prefix, descending)
} }
@ -42,8 +38,7 @@ export namespace Identifier {
} }
function randomBase62(length: number): string { function randomBase62(length: number): string {
const chars = const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
let result = "" let result = ""
const bytes = randomBytes(length) const bytes = randomBytes(length)
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
@ -52,10 +47,7 @@ export namespace Identifier {
return result return result
} }
function generateNewID( function generateNewID(prefix: keyof typeof prefixes, descending: boolean): string {
prefix: keyof typeof prefixes,
descending: boolean,
): string {
const currentTimestamp = Date.now() const currentTimestamp = Date.now()
if (currentTimestamp !== lastTimestamp) { if (currentTimestamp !== lastTimestamp) {
@ -73,11 +65,6 @@ export namespace Identifier {
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff)) timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
} }
return ( return prefixes[prefix] + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12)
prefixes[prefix] +
"_" +
timeBytes.toString("hex") +
randomBase62(LENGTH - 12)
)
} }
} }

View file

@ -55,10 +55,7 @@ const cli = yargs(hideBin(process.argv))
.command(ServeCommand) .command(ServeCommand)
.command(ModelsCommand) .command(ModelsCommand)
.fail((msg) => { .fail((msg) => {
if ( if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
msg.startsWith("Unknown argument") ||
msg.startsWith("Not enough non-option arguments")
) {
cli.showHelp("log") cli.showHelp("log")
} }
}) })
@ -97,10 +94,7 @@ try {
Log.Default.error("fatal", data) Log.Default.error("fatal", data)
const formatted = FormatError(e) const formatted = FormatError(e)
if (formatted) UI.error(formatted) if (formatted) UI.error(formatted)
if (formatted === undefined) if (formatted === undefined) UI.error("Unexpected error, check log file at " + Log.file() + " for more details")
UI.error(
"Unexpected error, check log file at " + Log.file() + " for more details",
)
process.exitCode = 1 process.exitCode = 1
} }

View file

@ -135,8 +135,7 @@ export namespace Installation {
}) })
} }
export const VERSION = export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
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")

View file

@ -1,9 +1,5 @@
import path from "path" import path from "path"
import { import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
createMessageConnection,
StreamMessageReader,
StreamMessageWriter,
} from "vscode-jsonrpc/node"
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types" import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
import { App } from "../app/app" import { App } from "../app/app"
import { Log } from "../util/log" import { Log } from "../util/log"
@ -66,6 +62,7 @@ export namespace LSPClient {
log.info("sending initialize", { id: serverID }) log.info("sending initialize", { id: serverID })
await withTimeout( await withTimeout(
connection.sendRequest("initialize", { connection.sendRequest("initialize", {
rootUri: "file://" + app.path.cwd,
processId: server.process.pid, processId: server.process.pid,
workspaceFolders: [ workspaceFolders: [
{ {
@ -120,9 +117,7 @@ export namespace LSPClient {
}, },
notify: { notify: {
async open(input: { path: string }) { async open(input: { path: string }) {
input.path = path.isAbsolute(input.path) input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path)
? input.path
: path.resolve(app.path.cwd, input.path)
const file = Bun.file(input.path) const file = Bun.file(input.path)
const text = await file.text() const text = await file.text()
const version = files[input.path] const version = files[input.path]
@ -154,18 +149,13 @@ export namespace LSPClient {
return diagnostics return diagnostics
}, },
async waitForDiagnostics(input: { path: string }) { async waitForDiagnostics(input: { path: string }) {
input.path = path.isAbsolute(input.path) input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path)
? input.path
: path.resolve(app.path.cwd, input.path)
log.info("waiting for diagnostics", input) log.info("waiting for diagnostics", input)
let unsub: () => void let unsub: () => void
return await withTimeout( return await withTimeout(
new Promise<void>((resolve) => { new Promise<void>((resolve) => {
unsub = Bus.subscribe(Event.Diagnostics, (event) => { unsub = Bus.subscribe(Event.Diagnostics, (event) => {
if ( if (event.properties.path === input.path && event.properties.serverID === result.serverID) {
event.properties.path === input.path &&
event.properties.serverID === result.serverID
) {
log.info("got diagnostics", input) log.info("got diagnostics", input)
unsub?.() unsub?.()
resolve() resolve()

View file

@ -46,9 +46,7 @@ export namespace LSP {
if (!file) continue if (!file) continue
const handle = await server.spawn(App.info()) const handle = await server.spawn(App.info())
if (!handle) break if (!handle) break
const client = await LSPClient.create(server.id, handle).catch( const client = await LSPClient.create(server.id, handle).catch((err) => log.error("", { error: err }))
(err) => log.error("", { error: err }),
)
if (!client) break if (!client) break
clients.set(server.id, client) clients.set(server.id, client)
break break
@ -77,9 +75,7 @@ export namespace LSP {
.map((x) => x.id) .map((x) => x.id)
await run(async (client) => { await run(async (client) => {
if (!matches.includes(client.serverID)) return if (!matches.includes(client.serverID)) return
const wait = waitForDiagnostics const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
? client.waitForDiagnostics({ path: input })
: Promise.resolve()
await client.notify.open({ path: input }) await client.notify.open({ path: input })
return wait return wait
}) })
@ -97,11 +93,7 @@ export namespace LSP {
return results return results
} }
export async function hover(input: { export async function hover(input: { file: string; line: number; character: number }) {
file: string
line: number
character: number
}) {
return run((client) => { return run((client) => {
return client.connection.sendRequest("textDocument/hover", { return client.connection.sendRequest("textDocument/hover", {
textDocument: { textDocument: {
@ -123,9 +115,7 @@ export namespace LSP {
).then((result) => result.flat() as LSP.Symbol[]) ).then((result) => result.flat() as LSP.Symbol[])
} }
async function run<T>( async function run<T>(input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
input: (client: LSPClient.Info) => Promise<T>,
): Promise<T[]> {
const clients = await state().then((x) => [...x.clients.values()]) const clients = await state().then((x) => [...x.clients.values()])
const tasks = clients.map((x) => input(x)) const tasks = clients.map((x) => input(x))
return Promise.all(tasks) return Promise.all(tasks)

View file

@ -25,21 +25,14 @@ export namespace LSPServer {
id: "typescript", id: "typescript",
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"], extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
async spawn(app) { async spawn(app) {
const tsserver = await Bun.resolve( const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {})
"typescript/lib/tsserver.js",
app.path.cwd,
).catch(() => {})
if (!tsserver) return if (!tsserver) return
const proc = spawn( const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
BunProc.which(),
["x", "typescript-language-server", "--stdio"],
{
env: { env: {
...process.env, ...process.env,
BUN_BE_BUN: "1", BUN_BE_BUN: "1",
}, },
}, })
)
return { return {
process: proc, process: proc,
initialization: { initialization: {
@ -73,10 +66,7 @@ export namespace LSPServer {
log.error("Failed to install gopls") log.error("Failed to install gopls")
return return
} }
bin = path.join( bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
Global.Path.bin,
"gopls" + (process.platform === "win32" ? ".exe" : ""),
)
log.info(`installed gopls`, { log.info(`installed gopls`, {
bin, bin,
}) })
@ -113,10 +103,7 @@ export namespace LSPServer {
log.error("Failed to install ruby-lsp") log.error("Failed to install ruby-lsp")
return return
} }
bin = path.join( bin = path.join(Global.Path.bin, "ruby-lsp" + (process.platform === "win32" ? ".exe" : ""))
Global.Path.bin,
"ruby-lsp" + (process.platform === "win32" ? ".exe" : ""),
)
log.info(`installed ruby-lsp`, { log.info(`installed ruby-lsp`, {
bin, bin,
}) })
@ -131,16 +118,12 @@ export namespace LSPServer {
id: "pyright", id: "pyright",
extensions: [".py", ".pyi"], extensions: [".py", ".pyi"],
async spawn() { async spawn() {
const proc = spawn( const proc = spawn(BunProc.which(), ["x", "pyright-langserver", "--stdio"], {
BunProc.which(),
["x", "pyright-langserver", "--stdio"],
{
env: { env: {
...process.env, ...process.env,
BUN_BE_BUN: "1", BUN_BE_BUN: "1",
}, },
}, })
)
return { return {
process: proc, process: proc,
} }
@ -158,9 +141,7 @@ export namespace LSPServer {
Global.Path.bin, Global.Path.bin,
"elixir-ls-master", "elixir-ls-master",
"release", "release",
process.platform === "win32" process.platform === "win32" ? "language_server.bar" : "language_server.sh",
? "language_server.bar"
: "language_server.sh",
) )
if (!(await Bun.file(binary).exists())) { if (!(await Bun.file(binary).exists())) {
@ -172,9 +153,7 @@ export namespace LSPServer {
log.info("downloading elixir-ls from GitHub releases") log.info("downloading elixir-ls from GitHub releases")
const response = await fetch( const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
"https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip",
)
if (!response.ok) return if (!response.ok) return
const zipPath = path.join(Global.Path.bin, "elixir-ls.zip") const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
await Bun.file(zipPath).write(response) await Bun.file(zipPath).write(response)

View file

@ -91,8 +91,7 @@ export namespace Provider {
if (!info || info.type !== "oauth") return if (!info || info.type !== "oauth") return
if (!info.access || info.expires < Date.now()) { if (!info.access || info.expires < Date.now()) {
const tokens = await copilot.access(info.refresh) const tokens = await copilot.access(info.refresh)
if (!tokens) if (!tokens) throw new Error("GitHub Copilot authentication expired")
throw new Error("GitHub Copilot authentication expired")
await Auth.set("github-copilot", { await Auth.set("github-copilot", {
type: "oauth", type: "oauth",
...tokens, ...tokens,
@ -101,15 +100,9 @@ export namespace Provider {
} }
let isAgentCall = false let isAgentCall = false
try { try {
const body = const body = typeof init.body === "string" ? JSON.parse(init.body) : init.body
typeof init.body === "string"
? JSON.parse(init.body)
: init.body
if (body?.messages) { if (body?.messages) {
isAgentCall = body.messages.some( isAgentCall = body.messages.some((msg: any) => msg.role && ["tool", "assistant"].includes(msg.role))
(msg: any) =>
msg.role && ["tool", "assistant"].includes(msg.role),
)
} }
} catch {} } catch {}
const headers = { const headers = {
@ -138,14 +131,11 @@ export namespace Provider {
} }
}, },
"amazon-bedrock": async () => { "amazon-bedrock": async () => {
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"]) if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"]) return { autoload: false }
return { autoload: false }
const region = process.env["AWS_REGION"] ?? "us-east-1" const region = process.env["AWS_REGION"] ?? "us-east-1"
const { fromNodeProviderChain } = await import( const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
await BunProc.install("@aws-sdk/credential-providers")
)
return { return {
autoload: true, autoload: true,
options: { options: {
@ -157,9 +147,7 @@ export namespace Provider {
switch (regionPrefix) { switch (regionPrefix) {
case "us": { case "us": {
const modelRequiresPrefix = ["claude", "deepseek"].some((m) => const modelRequiresPrefix = ["claude", "deepseek"].some((m) => modelID.includes(m))
modelID.includes(m),
)
if (modelRequiresPrefix) { if (modelRequiresPrefix) {
modelID = `${regionPrefix}.${modelID}` modelID = `${regionPrefix}.${modelID}`
} }
@ -174,25 +162,18 @@ export namespace Provider {
"eu-south-1", "eu-south-1",
"eu-south-2", "eu-south-2",
].some((r) => region.includes(r)) ].some((r) => region.includes(r))
const modelRequiresPrefix = [ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) =>
"claude", modelID.includes(m),
"nova-lite", )
"nova-micro",
"llama3",
"pixtral",
].some((m) => modelID.includes(m))
if (regionRequiresPrefix && modelRequiresPrefix) { if (regionRequiresPrefix && modelRequiresPrefix) {
modelID = `${regionPrefix}.${modelID}` modelID = `${regionPrefix}.${modelID}`
} }
break break
} }
case "ap": { case "ap": {
const modelRequiresPrefix = [ const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
"claude", modelID.includes(m),
"nova-lite", )
"nova-micro",
"nova-pro",
].some((m) => modelID.includes(m))
if (modelRequiresPrefix) { if (modelRequiresPrefix) {
regionPrefix = "apac" regionPrefix = "apac"
modelID = `${regionPrefix}.${modelID}` modelID = `${regionPrefix}.${modelID}`
@ -230,10 +211,7 @@ export namespace Provider {
options: Record<string, any> options: Record<string, any>
} }
} = {} } = {}
const models = new Map< const models = new Map<string, { info: ModelsDev.Model; language: LanguageModel }>()
string,
{ info: ModelsDev.Model; language: LanguageModel }
>()
const sdk = new Map<string, SDK>() const sdk = new Map<string, SDK>()
log.info("init") log.info("init")
@ -308,9 +286,7 @@ export namespace Provider {
database[providerID] = parsed database[providerID] = parsed
} }
const disabled = await Config.get().then( const disabled = await Config.get().then((cfg) => new Set(cfg.disabled_providers ?? []))
(cfg) => new Set(cfg.disabled_providers ?? []),
)
// load env // load env
for (const [providerID, provider] of Object.entries(database)) { for (const [providerID, provider] of Object.entries(database)) {
if (disabled.has(providerID)) continue if (disabled.has(providerID)) continue
@ -337,12 +313,7 @@ export namespace Provider {
if (disabled.has(providerID)) continue if (disabled.has(providerID)) continue
const result = await fn(database[providerID]) const result = await fn(database[providerID])
if (result && (result.autoload || providers[providerID])) { if (result && (result.autoload || providers[providerID])) {
mergeProvider( mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
providerID,
result.options ?? {},
"custom",
result.getModel,
)
} }
} }
@ -379,7 +350,7 @@ export namespace Provider {
const existing = s.sdk.get(provider.id) const existing = s.sdk.get(provider.id)
if (existing) return existing if (existing) return existing
const pkg = provider.npm ?? provider.id const pkg = provider.npm ?? provider.id
const mod = await import(await BunProc.install(pkg, "latest")) const mod = await import(await BunProc.install(pkg, "beta"))
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!] const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
const loaded = fn(s.providers[provider.id]?.options) const loaded = fn(s.providers[provider.id]?.options)
s.sdk.set(provider.id, loaded) s.sdk.set(provider.id, loaded)
@ -406,9 +377,7 @@ export namespace Provider {
const sdk = await getSDK(provider.info) const sdk = await getSDK(provider.info)
try { try {
const language = provider.getModel const language = provider.getModel ? await provider.getModel(sdk, modelID) : sdk.languageModel(modelID)
? await provider.getModel(sdk, modelID)
: sdk.languageModel(modelID)
log.info("found", { providerID, modelID }) log.info("found", { providerID, modelID })
s.models.set(key, { s.models.set(key, {
info, info,
@ -435,10 +404,7 @@ export namespace Provider {
export function sort(models: ModelsDev.Model[]) { export function sort(models: ModelsDev.Model[]) {
return sortBy( return sortBy(
models, models,
[ [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
(model) => priority.findIndex((filter) => model.id.includes(filter)),
"desc",
],
[(model) => (model.id.includes("latest") ? 0 : 1), "asc"], [(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
[(model) => model.id, "desc"], [(model) => model.id, "desc"],
) )
@ -449,11 +415,7 @@ export namespace Provider {
if (cfg.model) return parseModel(cfg.model) if (cfg.model) return parseModel(cfg.model)
const provider = await list() const provider = await list()
.then((val) => Object.values(val)) .then((val) => Object.values(val))
.then((x) => .then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)))
x.find(
(p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id),
),
)
if (!provider) throw new Error("no providers found") if (!provider) throw new Error("no providers found")
const [model] = sort(Object.values(provider.info.models)) const [model] = sort(Object.values(provider.info.models))
if (!model) throw new Error("no models found") if (!model) throw new Error("no models found")
@ -536,9 +498,11 @@ export namespace Provider {
if (schema instanceof z.ZodUnion) { if (schema instanceof z.ZodUnion) {
return z.union( return z.union(
schema.options.map((option: z.ZodTypeAny) => schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [
optionalToNullable(option), z.ZodTypeAny,
) as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]], z.ZodTypeAny,
...z.ZodTypeAny[],
],
) )
} }

View file

@ -1,22 +1,21 @@
import type { LanguageModelV1Prompt } from "ai" import type { ModelMessage } from "ai"
import { unique } from "remeda" import { unique } from "remeda"
export namespace ProviderTransform { export namespace ProviderTransform {
export function message( export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
msgs: LanguageModelV1Prompt,
providerID: string,
modelID: string,
) {
if (providerID === "anthropic" || modelID.includes("anthropic")) { if (providerID === "anthropic" || modelID.includes("anthropic")) {
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2) const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
const final = msgs.filter((msg) => msg.role !== "system").slice(-2) const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
for (const msg of unique([...system, ...final])) { for (const msg of unique([...system, ...final])) {
msg.providerMetadata = { msg.providerOptions = {
...msg.providerMetadata, ...msg.providerOptions,
anthropic: { anthropic: {
cacheControl: { type: "ephemeral" }, cacheControl: { type: "ephemeral" },
}, },
openaiCompatible: {
cache_control: { type: "ephemeral" },
},
} }
} }
} }
@ -25,8 +24,8 @@ export namespace ProviderTransform {
const final = msgs.filter((msg) => msg.role !== "system").slice(-2) const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
for (const msg of unique([...system, ...final])) { for (const msg of unique([...system, ...final])) {
msg.providerMetadata = { msg.providerOptions = {
...msg.providerMetadata, ...msg.providerOptions,
bedrock: { bedrock: {
cachePoint: { type: "ephemeral" }, cachePoint: { type: "ephemeral" },
}, },

View file

@ -6,7 +6,6 @@ import { streamSSE } from "hono/streaming"
import { Session } from "../session" import { Session } from "../session"
import { resolver, validator as zValidator } from "hono-openapi/zod" import { resolver, validator as zValidator } from "hono-openapi/zod"
import { z } from "zod" import { z } from "zod"
import { Message } from "../session/message"
import { Provider } from "../provider/provider" import { Provider } from "../provider/provider"
import { App } from "../app/app" import { App } from "../app/app"
import { mapValues } from "remeda" import { mapValues } from "remeda"
@ -16,6 +15,7 @@ import { Ripgrep } from "../file/ripgrep"
import { Config } from "../config/config" import { Config } from "../config/config"
import { File } from "../file" import { File } from "../file"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { MessageV2 } from "../session/message-v2"
const ERRORS = { const ERRORS = {
400: { 400: {
@ -51,12 +51,9 @@ export namespace Server {
status: 400, status: 400,
}) })
} }
return c.json( return c.json(new NamedError.Unknown({ message: err.toString() }).toObject(), {
new NamedError.Unknown({ message: err.toString() }).toObject(),
{
status: 400, status: 400,
}, })
)
}) })
.use(async (c, next) => { .use(async (c, next) => {
log.info("request", { log.info("request", {
@ -407,7 +404,7 @@ export namespace Server {
description: "List of messages", description: "List of messages",
content: { content: {
"application/json": { "application/json": {
schema: resolver(Message.Info.array()), schema: resolver(MessageV2.Info.array()),
}, },
}, },
}, },
@ -433,7 +430,7 @@ export namespace Server {
description: "Created message", description: "Created message",
content: { content: {
"application/json": { "application/json": {
schema: resolver(Message.Info), schema: resolver(MessageV2.Assistant),
}, },
}, },
}, },
@ -450,7 +447,7 @@ export namespace Server {
z.object({ z.object({
providerID: z.string(), providerID: z.string(),
modelID: z.string(), modelID: z.string(),
parts: Message.MessagePart.array(), parts: MessageV2.UserPart.array(),
}), }),
), ),
async (c) => { async (c) => {
@ -481,15 +478,10 @@ export namespace Server {
}, },
}), }),
async (c) => { async (c) => {
const providers = await Provider.list().then((x) => const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info))
mapValues(x, (item) => item.info),
)
return c.json({ return c.json({
providers: Object.values(providers), providers: Object.values(providers),
default: mapValues( default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
providers,
(item) => Provider.sort(Object.values(item.models))[0].id,
),
}) })
}, },
) )

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,398 @@
import z from "zod"
import { Bus } from "../bus"
import { Provider } from "../provider/provider"
import { NamedError } from "../util/error"
import { Message } from "./message"
import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
export namespace MessageV2 {
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
export const ToolStatePending = z
.object({
status: z.literal("pending"),
})
.openapi({
ref: "ToolStatePending",
})
export type ToolStatePending = z.infer<typeof ToolStatePending>
export const ToolStateRunning = z
.object({
status: z.literal("running"),
input: z.any(),
title: z.string().optional(),
metadata: z.record(z.any()).optional(),
time: z.object({
start: z.number(),
}),
})
.openapi({
ref: "ToolStateRunning",
})
export type ToolStateRunning = z.infer<typeof ToolStateRunning>
export const ToolStateCompleted = z
.object({
status: z.literal("completed"),
input: z.record(z.any()),
output: z.string(),
title: z.string(),
metadata: z.record(z.any()),
time: z.object({
start: z.number(),
end: z.number(),
}),
})
.openapi({
ref: "ToolStateCompleted",
})
export type ToolStateCompleted = z.infer<typeof ToolStateCompleted>
export const ToolStateError = z
.object({
status: z.literal("error"),
input: z.record(z.any()),
error: z.string(),
time: z.object({
start: z.number(),
end: z.number(),
}),
})
.openapi({
ref: "ToolStateError",
})
export type ToolStateError = z.infer<typeof ToolStateError>
export const ToolState = z
.discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError])
.openapi({
ref: "ToolState",
})
export const TextPart = z
.object({
type: z.literal("text"),
text: z.string(),
})
.openapi({
ref: "TextPart",
})
export type TextPart = z.infer<typeof TextPart>
export const ToolPart = z
.object({
type: z.literal("tool"),
id: z.string(),
tool: z.string(),
state: ToolState,
})
.openapi({
ref: "ToolPart",
})
export type ToolPart = z.infer<typeof ToolPart>
export const FilePart = z
.object({
type: z.literal("file"),
mime: z.string(),
filename: z.string().optional(),
url: z.string(),
})
.openapi({
ref: "FilePart",
})
export type FilePart = z.infer<typeof FilePart>
export const StepStartPart = z
.object({
type: z.literal("step-start"),
})
.openapi({
ref: "StepStartPart",
})
export type StepStartPart = z.infer<typeof StepStartPart>
const Base = z.object({
id: z.string(),
sessionID: z.string(),
})
export const UserPart = z.discriminatedUnion("type", [TextPart, FilePart]).openapi({
ref: "UserMessagePart",
})
export type UserPart = z.infer<typeof UserPart>
export const User = Base.extend({
role: z.literal("user"),
parts: z.array(UserPart),
time: z.object({
created: z.number(),
}),
}).openapi({
ref: "UserMessage",
})
export type User = z.infer<typeof User>
export const AssistantPart = z.discriminatedUnion("type", [TextPart, ToolPart, StepStartPart]).openapi({
ref: "AssistantMessagePart",
})
export type AssistantPart = z.infer<typeof AssistantPart>
export const Assistant = Base.extend({
role: z.literal("assistant"),
parts: z.array(AssistantPart),
time: z.object({
created: z.number(),
completed: z.number().optional(),
}),
error: z
.discriminatedUnion("name", [Provider.AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema])
.optional(),
system: z.string().array(),
modelID: z.string(),
providerID: z.string(),
path: z.object({
cwd: z.string(),
root: z.string(),
}),
cost: z.number(),
summary: z.boolean().optional(),
tokens: z.object({
input: z.number(),
output: z.number(),
reasoning: z.number(),
cache: z.object({
read: z.number(),
write: z.number(),
}),
}),
}).openapi({
ref: "AssistantMessage",
})
export type Assistant = z.infer<typeof Assistant>
export const Info = z.discriminatedUnion("role", [User, Assistant]).openapi({
ref: "Message",
})
export type Info = z.infer<typeof Info>
export const Event = {
Updated: Bus.event(
"message.updated",
z.object({
info: Info,
}),
),
Removed: Bus.event(
"message.removed",
z.object({
sessionID: z.string(),
messageID: z.string(),
}),
),
PartUpdated: Bus.event(
"message.part.updated",
z.object({
part: AssistantPart,
sessionID: z.string(),
messageID: z.string(),
}),
),
}
export function fromV1(v1: Message.Info) {
if (v1.role === "assistant") {
const result: Assistant = {
id: v1.id,
sessionID: v1.metadata.sessionID,
role: "assistant",
time: {
created: v1.metadata.time.created,
completed: v1.metadata.time.completed,
},
cost: v1.metadata.assistant!.cost,
path: v1.metadata.assistant!.path,
summary: v1.metadata.assistant!.summary,
tokens: v1.metadata.assistant!.tokens,
modelID: v1.metadata.assistant!.modelID,
providerID: v1.metadata.assistant!.providerID,
system: v1.metadata.assistant!.system,
error: v1.metadata.error,
parts: v1.parts.flatMap((part): AssistantPart[] => {
if (part.type === "text") {
return [
{
type: "text",
text: part.text,
},
]
}
if (part.type === "step-start") {
return [
{
type: "step-start",
},
]
}
if (part.type === "tool-invocation") {
return [
{
type: "tool",
id: part.toolInvocation.toolCallId,
tool: part.toolInvocation.toolName,
state: (() => {
if (part.toolInvocation.state === "partial-call") {
return {
status: "pending",
}
}
const { title, time, ...metadata } = v1.metadata.tool[part.toolInvocation.toolCallId] ?? {}
if (part.toolInvocation.state === "call") {
return {
status: "running",
input: part.toolInvocation.args,
time: {
start: time?.start,
},
}
}
if (part.toolInvocation.state === "result") {
return {
status: "completed",
input: part.toolInvocation.args,
output: part.toolInvocation.result,
title,
time,
metadata,
}
}
throw new Error("unknown tool invocation state")
})(),
},
]
}
return []
}),
}
return result
}
if (v1.role === "user") {
const result: User = {
id: v1.id,
sessionID: v1.metadata.sessionID,
role: "user",
time: {
created: v1.metadata.time.created,
},
parts: v1.parts.flatMap((part): UserPart[] => {
if (part.type === "text") {
return [
{
type: "text",
text: part.text,
},
]
}
if (part.type === "file") {
return [
{
type: "file",
mime: part.mediaType,
filename: part.filename,
url: part.url,
},
]
}
return []
}),
}
return result
}
}
export function toModelMessage(input: Info[]): ModelMessage[] {
const result: UIMessage[] = []
for (const msg of input) {
if (msg.parts.length === 0) continue
if (msg.role === "user") {
result.push({
id: msg.id,
role: "user",
parts: msg.parts.flatMap((part): UIMessage["parts"] => {
if (part.type === "text")
return [
{
type: "text",
text: part.text,
},
]
if (part.type === "file")
return [
{
type: "file",
url: part.url,
mediaType: part.mime,
filename: part.filename,
},
]
return []
}),
})
}
if (msg.role === "assistant") {
result.push({
id: msg.id,
role: "assistant",
parts: msg.parts.flatMap((part): UIMessage["parts"] => {
if (part.type === "text")
return [
{
type: "text",
text: part.text,
},
]
if (part.type === "step-start")
return [
{
type: "step-start",
},
]
if (part.type === "tool") {
if (part.state.status === "completed")
return [
{
type: ("tool-" + part.tool) as `tool-${string}`,
state: "output-available",
toolCallId: part.id,
input: part.state.input,
output: part.state.output,
},
]
if (part.state.status === "error")
return [
{
type: ("tool-" + part.tool) as `tool-${string}`,
state: "output-error",
toolCallId: part.id,
input: part.state.input,
errorText: part.state.error,
},
]
}
return []
}),
})
}
}
return convertToModelMessages(result)
}
}

View file

@ -1,13 +1,9 @@
import z from "zod" import z from "zod"
import { Bus } from "../bus"
import { Provider } from "../provider/provider" 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( export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
"MessageOutputLengthError",
z.object({}),
)
export const ToolCall = z export const ToolCall = z
.object({ .object({
@ -49,9 +45,7 @@ export namespace Message {
}) })
export type ToolResult = z.infer<typeof ToolResult> export type ToolResult = z.infer<typeof ToolResult>
export const ToolInvocation = z export const ToolInvocation = z.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]).openapi({
.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult])
.openapi({
ref: "ToolInvocation", ref: "ToolInvocation",
}) })
export type ToolInvocation = z.infer<typeof ToolInvocation> export type ToolInvocation = z.infer<typeof ToolInvocation>
@ -122,14 +116,7 @@ export namespace Message {
export type StepStartPart = z.infer<typeof StepStartPart> export type StepStartPart = z.infer<typeof StepStartPart>
export const MessagePart = z export const MessagePart = z
.discriminatedUnion("type", [ .discriminatedUnion("type", [TextPart, ReasoningPart, ToolInvocationPart, SourceUrlPart, FilePart, StepStartPart])
TextPart,
ReasoningPart,
ToolInvocationPart,
SourceUrlPart,
FilePart,
StepStartPart,
])
.openapi({ .openapi({
ref: "MessagePart", ref: "MessagePart",
}) })
@ -197,28 +184,4 @@ export namespace Message {
ref: "Message", ref: "Message",
}) })
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
export const Event = {
Updated: Bus.event(
"message.updated",
z.object({
info: Info,
}),
),
Removed: Bus.event(
"message.removed",
z.object({
sessionID: z.string(),
messageID: z.string(),
}),
),
PartUpdated: Bus.event(
"message.part.updated",
z.object({
part: MessagePart,
sessionID: z.string(),
messageID: z.string(),
}),
),
}
} }

View file

@ -53,9 +53,7 @@ export namespace Share {
export const URL = export const URL =
process.env["OPENCODE_API"] ?? process.env["OPENCODE_API"] ??
(Installation.isSnapshot() || Installation.isDev() (Installation.isSnapshot() || Installation.isDev() ? "https://api.dev.opencode.ai" : "https://api.opencode.ai")
? "https://api.dev.opencode.ai"
: "https://api.opencode.ai")
export async function create(sessionID: string) { export async function create(sessionID: string) {
return fetch(`${URL}/share_create`, { return fetch(`${URL}/share_create`, {

View file

@ -1,14 +1,7 @@
import { App } from "../app/app" import { App } from "../app/app"
import { import { $ } from "bun"
add,
commit,
init,
checkout,
statusMatrix,
remove,
} from "isomorphic-git"
import path from "path" import path from "path"
import fs from "fs" import fs from "fs/promises"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file/ripgrep"
import { Log } from "../util/log" import { Log } from "../util/log"
@ -16,79 +9,53 @@ 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) const git = gitdir(sessionID)
// not a git repo, check if too big to snapshot
if (!app.git) {
const files = await Ripgrep.files({ const files = await Ripgrep.files({
cwd: app.path.cwd, cwd: app.path.cwd,
limit: app.git ? undefined : 1000, limit: 1000,
}) })
log.info("found files", { count: files.length }) log.info("found files", { count: files.length })
// not a git repo and too big to snapshot if (files.length > 1000) return
if (!app.git && files.length === 1000) return }
await init({
dir: app.path.cwd, if (await fs.mkdir(git, { recursive: true })) {
gitdir: git, await $`git init`
fs, .env({
...process.env,
GIT_DIR: git,
GIT_WORK_TREE: app.path.root,
}) })
.quiet()
.nothrow()
log.info("initialized") log.info("initialized")
const status = await statusMatrix({
fs,
gitdir: git,
dir: app.path.cwd,
})
log.info("matrix", {
count: status.length,
})
const added = []
for (const [file, head, workdir, stage] of status) {
if (workdir === 0 && stage === 1) {
log.info("remove", { file })
await remove({
fs,
gitdir: git,
dir: app.path.cwd,
filepath: file,
})
continue
} }
if (workdir !== head) {
added.push(file) await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow()
}
}
log.info("removed files")
await add({
fs,
gitdir: git,
parallel: true,
dir: app.path.cwd,
filepath: added,
})
log.info("added files") log.info("added files")
const result = await commit({
fs, const result =
gitdir: git, await $`git --git-dir ${git} commit --allow-empty -m "snapshot" --author="opencode <mail@opencode.ai>"`
dir: app.path.cwd, .quiet()
message: "snapshot", .cwd(app.path.cwd)
author: { .nothrow()
name: "opencode", log.info("commit")
email: "mail@opencode.ai",
}, const match = result.stdout.toString().match(/\[.+ ([a-f0-9]+)\]/)
}) if (!match) return
log.info("commit", { result }) return match![1]
return result
} }
export async function restore(sessionID: string, commit: string) { export async function restore(sessionID: string, commit: string) {
log.info("restore", { commit }) log.info("restore", { commit })
const app = App.info() const app = App.info()
await checkout({ const git = gitdir(sessionID)
fs, await $`git --git-dir=${git} checkout ${commit} --force`.quiet().cwd(app.path.root)
gitdir: gitdir(sessionID),
dir: app.path.cwd,
ref: commit,
force: true,
})
} }
function gitdir(sessionID: string) { function gitdir(sessionID: string) {

View file

@ -4,44 +4,79 @@ import { Bus } from "../bus"
import path from "path" import path from "path"
import z from "zod" import z from "zod"
import fs from "fs/promises" import fs from "fs/promises"
import { MessageV2 } from "../session/message-v2"
export namespace Storage { export namespace Storage {
const log = Log.create({ service: "storage" }) const log = Log.create({ service: "storage" })
export const Event = { export const Event = {
Write: Bus.event( Write: Bus.event("storage.write", z.object({ key: z.string(), content: z.any() })),
"storage.write",
z.object({ key: z.string(), content: z.any() }),
),
} }
const state = App.state("storage", () => { type Migration = (dir: string) => Promise<void>
const MIGRATIONS: Migration[] = [
async (dir: string) => {
try {
const files = new Bun.Glob("session/message/*/*.json").scanSync({
cwd: dir,
absolute: true,
})
for (const file of files) {
const content = await Bun.file(file).json()
if (!content.metadata) continue
log.info("migrating to v2 message", { file })
try {
const result = MessageV2.fromV1(content)
await Bun.write(file, JSON.stringify(result, null, 2))
} catch (e) {
await fs.rename(file, file.replace("storage", "broken"))
}
}
} catch {}
},
]
const state = App.state("storage", async () => {
const app = App.info() const app = App.info()
const dir = path.join(app.path.data, "storage") const dir = path.join(app.path.data, "storage")
log.info("init", { path: dir }) const migration = await Bun.file(path.join(dir, "migration"))
.json()
.then((x) => parseInt(x))
.catch(() => 0)
for (let index = migration; index < MIGRATIONS.length; index++) {
log.info("running migration", { index })
const migration = MIGRATIONS[index]
await migration(dir)
await Bun.write(path.join(dir, "migration"), (index + 1).toString())
}
return { return {
dir, dir,
} }
}) })
export async function remove(key: string) { export async function remove(key: string) {
const target = path.join(state().dir, key + ".json") const dir = await state().then((x) => x.dir)
const target = path.join(dir, key + ".json")
await fs.unlink(target).catch(() => {}) await fs.unlink(target).catch(() => {})
} }
export async function removeDir(key: string) { export async function removeDir(key: string) {
const target = path.join(state().dir, key) const dir = await state().then((x) => x.dir)
const target = path.join(dir, key)
await fs.rm(target, { recursive: true, force: true }).catch(() => {}) await fs.rm(target, { recursive: true, force: true }).catch(() => {})
} }
export async function readJSON<T>(key: string) { export async function readJSON<T>(key: string) {
return Bun.file(path.join(state().dir, key + ".json")).json() as Promise<T> const dir = await state().then((x) => x.dir)
return Bun.file(path.join(dir, key + ".json")).json() as Promise<T>
} }
export async function writeJSON<T>(key: string, content: T) { export async function writeJSON<T>(key: string, content: T) {
const target = path.join(state().dir, key + ".json") const dir = await state().then((x) => x.dir)
const target = path.join(dir, key + ".json")
const tmp = target + Date.now() + ".tmp" const tmp = target + Date.now() + ".tmp"
await Bun.write(tmp, JSON.stringify(content)) await Bun.write(tmp, JSON.stringify(content, null, 2))
await fs.rename(tmp, target).catch(() => {}) await fs.rename(tmp, target).catch(() => {})
await fs.unlink(tmp).catch(() => {}) await fs.unlink(tmp).catch(() => {})
Bus.publish(Event.Write, { key, content }) Bus.publish(Event.Write, { key, content })
@ -49,9 +84,10 @@ 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)
try { try {
for await (const item of glob.scan({ for await (const item of glob.scan({
cwd: path.join(state().dir, prefix), cwd: path.join(dir, prefix),
onlyFiles: true, onlyFiles: true,
})) { })) {
const result = path.join(prefix, item.slice(0, -5)) const result = path.join(prefix, item.slice(0, -5))

View file

@ -4,25 +4,6 @@ import DESCRIPTION from "./bash.txt"
import { App } from "../app/app" import { App } from "../app/app"
const MAX_OUTPUT_LENGTH = 30000 const MAX_OUTPUT_LENGTH = 30000
const BANNED_COMMANDS = [
"alias",
"curl",
"curlie",
"wget",
"axel",
"aria2c",
"nc",
"telnet",
"lynx",
"w3m",
"links",
"httpie",
"xh",
"http-prompt",
"chrome",
"firefox",
"safari",
]
const DEFAULT_TIMEOUT = 1 * 60 * 1000 const DEFAULT_TIMEOUT = 1 * 60 * 1000
const MAX_TIMEOUT = 10 * 60 * 1000 const MAX_TIMEOUT = 10 * 60 * 1000
@ -31,12 +12,7 @@ export const BashTool = Tool.define({
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
command: z.string().describe("The command to execute"), command: z.string().describe("The command to execute"),
timeout: z timeout: z.number().min(0).max(MAX_TIMEOUT).describe("Optional timeout in milliseconds").optional(),
.number()
.min(0)
.max(MAX_TIMEOUT)
.describe("Optional timeout in milliseconds")
.optional(),
description: z description: z
.string() .string()
.describe( .describe(
@ -45,8 +21,6 @@ export const BashTool = Tool.define({
}), }),
async execute(params, ctx) { async execute(params, ctx) {
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT) const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
if (BANNED_COMMANDS.some((item) => params.command.startsWith(item)))
throw new Error(`Command '${params.command}' is not allowed`)
const process = Bun.spawn({ const process = Bun.spawn({
cmd: ["bash", "-c", params.command], cmd: ["bash", "-c", params.command],
@ -62,21 +36,14 @@ export const BashTool = Tool.define({
const stderr = await new Response(process.stderr).text() const stderr = await new Response(process.stderr).text()
return { return {
title: params.command,
metadata: { metadata: {
stderr, stderr,
stdout, stdout,
exit: process.exitCode, exit: process.exitCode,
description: params.description, description: params.description,
title: params.command,
}, },
output: [ output: [`<stdout>`, stdout ?? "", `</stdout>`, `<stderr>`, stderr ?? "", `</stderr>`].join("\n"),
`<stdout>`,
stdout ?? "",
`</stdout>`,
`<stderr>`,
stderr ?? "",
`</stderr>`,
].join("\n"),
} }
}, },
}) })

View file

@ -20,15 +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 newString: z.string().describe("The text to replace it with (must be different from old_string)"),
.string() replaceAll: z.boolean().optional().describe("Replace all occurrences of old_string (default false)"),
.describe(
"The text to replace it with (must be different from old_string)",
),
replaceAll: z
.boolean()
.optional()
.describe("Replace all occurrences of old_string (default false)"),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
if (!params.filePath) { if (!params.filePath) {
@ -40,9 +33,7 @@ export const EditTool = Tool.define({
} }
const app = App.info() const app = App.info()
const filepath = path.isAbsolute(params.filePath) const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
? params.filePath
: path.join(app.path.cwd, params.filePath)
await Permission.ask({ await Permission.ask({
id: "edit", id: "edit",
@ -70,17 +61,11 @@ export const EditTool = Tool.define({
const file = Bun.file(filepath) const file = Bun.file(filepath)
const stats = await file.stat().catch(() => {}) const stats = await file.stat().catch(() => {})
if (!stats) throw new Error(`File ${filepath} not found`) if (!stats) throw new Error(`File ${filepath} not found`)
if (stats.isDirectory()) if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filepath}`)
throw new Error(`Path is a directory, not a file: ${filepath}`)
await FileTime.assert(ctx.sessionID, filepath) await FileTime.assert(ctx.sessionID, filepath)
contentOld = await file.text() contentOld = await file.text()
contentNew = replace( contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
contentOld,
params.oldString,
params.newString,
params.replaceAll,
)
await file.write(contentNew) await file.write(contentNew)
await Bus.publish(File.Event.Edited, { await Bus.publish(File.Event.Edited, {
file: filepath, file: filepath,
@ -88,9 +73,7 @@ export const EditTool = Tool.define({
contentNew = await file.text() contentNew = await file.text()
})() })()
const diff = trimDiff( const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, contentNew))
createTwoFilesPatch(filepath, filepath, contentOld, contentNew),
)
FileTime.read(ctx.sessionID, filepath) FileTime.read(ctx.sessionID, filepath)
@ -110,17 +93,14 @@ export const EditTool = Tool.define({
metadata: { metadata: {
diagnostics, diagnostics,
diff, diff,
title: `${path.relative(app.path.root, filepath)}`,
}, },
title: `${path.relative(app.path.root, filepath)}`,
output, output,
} }
}, },
}) })
export type Replacer = ( export type Replacer = (content: string, find: string) => Generator<string, void, unknown>
content: string,
find: string,
) => Generator<string, void, unknown>
export const SimpleReplacer: Replacer = function* (_content, find) { export const SimpleReplacer: Replacer = function* (_content, find) {
yield find yield find
@ -208,10 +188,7 @@ export const BlockAnchorReplacer: Replacer = function* (content, find) {
} }
} }
export const WhitespaceNormalizedReplacer: Replacer = function* ( export const WhitespaceNormalizedReplacer: Replacer = function* (content, find) {
content,
find,
) {
const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim() const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim()
const normalizedFind = normalizeWhitespace(find) const normalizedFind = normalizeWhitespace(find)
@ -229,9 +206,7 @@ export const WhitespaceNormalizedReplacer: Replacer = function* (
// Find the actual substring in the original line that matches // Find the actual substring in the original line that matches
const words = find.trim().split(/\s+/) const words = find.trim().split(/\s+/)
if (words.length > 0) { if (words.length > 0) {
const pattern = words const pattern = words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+")
.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
.join("\\s+")
try { try {
const regex = new RegExp(pattern) const regex = new RegExp(pattern)
const match = line.match(regex) const match = line.match(regex)
@ -270,9 +245,7 @@ export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
}), }),
) )
return lines return lines.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent))).join("\n")
.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent)))
.join("\n")
} }
const normalizedFind = removeIndentation(find) const normalizedFind = removeIndentation(find)
@ -423,10 +396,7 @@ export const ContextAwareReplacer: Replacer = function* (content, find) {
} }
} }
if ( if (totalNonEmptyLines === 0 || matchingLines / totalNonEmptyLines >= 0.5) {
totalNonEmptyLines === 0 ||
matchingLines / totalNonEmptyLines >= 0.5
) {
yield block yield block
break // Only match the first occurrence break // Only match the first occurrence
} }
@ -473,12 +443,7 @@ function trimDiff(diff: string): string {
return trimmedLines.join("\n") return trimmedLines.join("\n")
} }
export function replace( export function replace(content: string, oldString: string, newString: string, replaceAll = false): string {
content: string,
oldString: string,
newString: string,
replaceAll = false,
): string {
if (oldString === newString) { if (oldString === newString) {
throw new Error("oldString and newString must be different") throw new Error("oldString and newString must be different")
} }
@ -502,11 +467,7 @@ export function replace(
} }
const lastIndex = content.lastIndexOf(search) const lastIndex = content.lastIndexOf(search)
if (index !== lastIndex) continue if (index !== lastIndex) continue
return ( return content.substring(0, index) + newString + content.substring(index + search.length)
content.substring(0, index) +
newString +
content.substring(index + search.length)
)
} }
} }
throw new Error("oldString not found in content or was found multiple times") throw new Error("oldString not found in content or was found multiple times")

View file

@ -20,9 +20,7 @@ export const GlobTool = Tool.define({
async execute(params) { async execute(params) {
const app = App.info() const app = App.info()
let search = params.path ?? app.path.cwd let search = params.path ?? app.path.cwd
search = path.isAbsolute(search) search = path.isAbsolute(search) ? search : path.resolve(app.path.cwd, search)
? search
: path.resolve(app.path.cwd, search)
const limit = 100 const limit = 100
const files = [] const files = []
@ -53,17 +51,15 @@ export const GlobTool = Tool.define({
output.push(...files.map((f) => f.path)) output.push(...files.map((f) => f.path))
if (truncated) { if (truncated) {
output.push("") output.push("")
output.push( output.push("(Results are truncated. Consider using a more specific path or pattern.)")
"(Results are truncated. Consider using a more specific path or pattern.)",
)
} }
} }
return { return {
title: path.relative(app.path.root, search),
metadata: { metadata: {
count: files.length, count: files.length,
truncated, truncated,
title: path.relative(app.path.root, search),
}, },
output: output.join("\n"), output: output.join("\n"),
} }

View file

@ -9,21 +9,9 @@ export const GrepTool = Tool.define({
id: "grep", id: "grep",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
pattern: z pattern: z.string().describe("The regex pattern to search for in file contents"),
.string() path: z.string().optional().describe("The directory to search in. Defaults to the current working directory."),
.describe("The regex pattern to search for in file contents"), include: z.string().optional().describe('File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")'),
path: z
.string()
.optional()
.describe(
"The directory to search in. Defaults to the current working directory.",
),
include: z
.string()
.optional()
.describe(
'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
),
}), }),
async execute(params) { async execute(params) {
if (!params.pattern) { if (!params.pattern) {
@ -51,7 +39,8 @@ export const GrepTool = Tool.define({
if (exitCode === 1) { if (exitCode === 1) {
return { return {
metadata: { matches: 0, truncated: false, title: params.pattern }, title: params.pattern,
metadata: { matches: 0, truncated: false },
output: "No files found", output: "No files found",
} }
} }
@ -93,7 +82,8 @@ export const GrepTool = Tool.define({
if (finalMatches.length === 0) { if (finalMatches.length === 0) {
return { return {
metadata: { matches: 0, truncated: false, title: params.pattern }, title: params.pattern,
metadata: { matches: 0, truncated: false },
output: "No files found", output: "No files found",
} }
} }
@ -114,16 +104,14 @@ export const GrepTool = Tool.define({
if (truncated) { if (truncated) {
outputLines.push("") outputLines.push("")
outputLines.push( outputLines.push("(Results are truncated. Consider using a more specific path or pattern.)")
"(Results are truncated. Consider using a more specific path or pattern.)",
)
} }
return { return {
title: params.pattern,
metadata: { metadata: {
matches: finalMatches.length, matches: finalMatches.length,
truncated, truncated,
title: params.pattern,
}, },
output: outputLines.join("\n"), output: outputLines.join("\n"),
} }

View file

@ -24,16 +24,8 @@ export const ListTool = Tool.define({
id: "list", id: "list",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
path: z path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(),
.string() ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(),
.describe(
"The absolute path to the directory to list (must be absolute, not relative)",
)
.optional(),
ignore: z
.array(z.string())
.describe("List of glob patterns to ignore")
.optional(),
}), }),
async execute(params) { async execute(params) {
const app = App.info() const app = App.info()
@ -44,8 +36,7 @@ export const ListTool = Tool.define({
for await (const file of glob.scan({ cwd: searchPath, dot: true })) { for await (const file of glob.scan({ cwd: searchPath, dot: true })) {
if (IGNORE_PATTERNS.some((p) => file.includes(p))) continue if (IGNORE_PATTERNS.some((p) => file.includes(p))) continue
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file))) if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file))) continue
continue
files.push(file) files.push(file)
if (files.length >= LIMIT) break if (files.length >= LIMIT) break
} }
@ -99,10 +90,10 @@ export const ListTool = Tool.define({
const output = `${searchPath}/\n` + renderDir(".", 0) const output = `${searchPath}/\n` + renderDir(".", 0)
return { return {
title: path.relative(app.path.root, searchPath),
metadata: { metadata: {
count: files.length, count: files.length,
truncated: files.length >= LIMIT, truncated: files.length >= LIMIT,
title: path.relative(app.path.root, searchPath),
}, },
output, output,
} }

View file

@ -13,20 +13,16 @@ export const LspDiagnosticTool = Tool.define({
}), }),
execute: async (args) => { execute: async (args) => {
const app = App.info() const app = App.info()
const normalized = path.isAbsolute(args.path) const normalized = path.isAbsolute(args.path) ? args.path : path.join(app.path.cwd, args.path)
? args.path
: path.join(app.path.cwd, args.path)
await LSP.touchFile(normalized, true) await LSP.touchFile(normalized, true)
const diagnostics = await LSP.diagnostics() const diagnostics = await LSP.diagnostics()
const file = diagnostics[normalized] const file = diagnostics[normalized]
return { return {
title: path.relative(app.path.root, normalized),
metadata: { metadata: {
diagnostics, diagnostics,
title: path.relative(app.path.root, normalized),
}, },
output: file?.length output: file?.length ? file.map(LSP.Diagnostic.pretty).join("\n") : "No errors found",
? file.map(LSP.Diagnostic.pretty).join("\n")
: "No errors found",
} }
}, },
}) })

View file

@ -15,9 +15,7 @@ export const LspHoverTool = Tool.define({
}), }),
execute: async (args) => { execute: async (args) => {
const app = App.info() const app = App.info()
const file = path.isAbsolute(args.file) const file = path.isAbsolute(args.file) ? args.file : path.join(app.path.cwd, args.file)
? args.file
: path.join(app.path.cwd, args.file)
await LSP.touchFile(file, true) await LSP.touchFile(file, true)
const result = await LSP.hover({ const result = await LSP.hover({
...args, ...args,
@ -25,14 +23,9 @@ export const LspHoverTool = Tool.define({
}) })
return { return {
title: path.relative(app.path.root, file) + ":" + args.line + ":" + args.character,
metadata: { metadata: {
result, result,
title:
path.relative(app.path.root, file) +
":" +
args.line +
":" +
args.character,
}, },
output: JSON.stringify(result, null, 2), output: JSON.stringify(result, null, 2),
} }

View file

@ -10,9 +10,7 @@ export const MultiEditTool = Tool.define({
description: DESCRIPTION, description: DESCRIPTION,
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"),
edits: z edits: z.array(EditTool.parameters).describe("Array of edit operations to perform sequentially on the file"),
.array(EditTool.parameters)
.describe("Array of edit operations to perform sequentially on the file"),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
const results = [] const results = []
@ -30,9 +28,9 @@ export const MultiEditTool = Tool.define({
} }
const app = App.info() const app = App.info()
return { return {
title: path.relative(app.path.root, params.filePath),
metadata: { metadata: {
results: results.map((r) => r.metadata), results: results.map((r) => r.metadata),
title: path.relative(app.path.root, params.filePath),
}, },
output: results.at(-1)!.output, output: results.at(-1)!.output,
} }

View file

@ -6,9 +6,7 @@ import { FileTime } from "../file/time"
import DESCRIPTION from "./patch.txt" import DESCRIPTION from "./patch.txt"
const PatchParams = z.object({ const PatchParams = z.object({
patchText: z patchText: z.string().describe("The full patch text that describes all changes to be made"),
.string()
.describe("The full patch text that describes all changes to be made"),
}) })
interface Change { interface Change {
@ -42,10 +40,7 @@ function identifyFilesNeeded(patchText: string): string[] {
const files: string[] = [] const files: string[] = []
const lines = patchText.split("\n") const lines = patchText.split("\n")
for (const line of lines) { for (const line of lines) {
if ( if (line.startsWith("*** Update File:") || line.startsWith("*** Delete File:")) {
line.startsWith("*** Update File:") ||
line.startsWith("*** Delete File:")
) {
const filePath = line.split(":", 2)[1]?.trim() const filePath = line.split(":", 2)[1]?.trim()
if (filePath) files.push(filePath) if (filePath) files.push(filePath)
} }
@ -65,10 +60,7 @@ function identifyFilesAdded(patchText: string): string[] {
return files return files
} }
function textToPatch( function textToPatch(patchText: string, _currentFiles: Record<string, string>): [PatchOperation[], number] {
patchText: string,
_currentFiles: Record<string, string>,
): [PatchOperation[], number] {
const operations: PatchOperation[] = [] const operations: PatchOperation[] = []
const lines = patchText.split("\n") const lines = patchText.split("\n")
let i = 0 let i = 0
@ -93,11 +85,7 @@ function textToPatch(
const changes: PatchChange[] = [] const changes: PatchChange[] = []
i++ i++
while ( while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("***")) {
i < lines.length &&
!lines[i].startsWith("@@") &&
!lines[i].startsWith("***")
) {
const changeLine = lines[i] const changeLine = lines[i]
if (changeLine.startsWith(" ")) { if (changeLine.startsWith(" ")) {
changes.push({ type: "keep", content: changeLine.substring(1) }) changes.push({ type: "keep", content: changeLine.substring(1) })
@ -151,10 +139,7 @@ function textToPatch(
return [operations, fuzz] return [operations, fuzz]
} }
function patchToCommit( function patchToCommit(operations: PatchOperation[], currentFiles: Record<string, string>): Commit {
operations: PatchOperation[],
currentFiles: Record<string, string>,
): Commit {
const changes: Record<string, Change> = {} const changes: Record<string, Change> = {}
for (const op of operations) { for (const op of operations) {
@ -173,9 +158,7 @@ function patchToCommit(
const lines = originalContent.split("\n") const lines = originalContent.split("\n")
for (const hunk of op.hunks) { for (const hunk of op.hunks) {
const contextIndex = lines.findIndex((line) => const contextIndex = lines.findIndex((line) => line.includes(hunk.contextLine))
line.includes(hunk.contextLine),
)
if (contextIndex === -1) { if (contextIndex === -1) {
throw new Error(`Context line not found: ${hunk.contextLine}`) throw new Error(`Context line not found: ${hunk.contextLine}`)
} }
@ -204,11 +187,7 @@ function patchToCommit(
return { changes } return { changes }
} }
function generateDiff( function generateDiff(oldContent: string, newContent: string, filePath: string): [string, number, number] {
oldContent: string,
newContent: string,
filePath: string,
): [string, number, number] {
// Mock implementation - would need actual diff generation // Mock implementation - would need actual diff generation
const lines1 = oldContent.split("\n") const lines1 = oldContent.split("\n")
const lines2 = newContent.split("\n") const lines2 = newContent.split("\n")
@ -296,9 +275,7 @@ export const PatchTool = Tool.define({
// Process the patch // Process the patch
const [patch, fuzz] = textToPatch(params.patchText, currentFiles) const [patch, fuzz] = textToPatch(params.patchText, currentFiles)
if (fuzz > 3) { if (fuzz > 3) {
throw new Error( throw new Error(`patch contains fuzzy matches (fuzz level: ${fuzz}). Please make your context lines more precise`)
`patch contains fuzzy matches (fuzz level: ${fuzz}). Please make your context lines more precise`,
)
} }
// Convert patch to commit // Convert patch to commit
@ -343,11 +320,7 @@ export const PatchTool = Tool.define({
const newContent = change.new_content || "" const newContent = change.new_content || ""
// Calculate diff statistics // Calculate diff statistics
const [, additions, removals] = generateDiff( const [, additions, removals] = generateDiff(oldContent, newContent, filePath)
oldContent,
newContent,
filePath,
)
totalAdditions += additions totalAdditions += additions
totalRemovals += removals totalRemovals += removals
@ -358,11 +331,11 @@ export const PatchTool = Tool.define({
const output = result const output = result
return { return {
title: `${filesToRead.length} files`,
metadata: { metadata: {
changed: changedFiles, changed: changedFiles,
additions: totalAdditions, additions: totalAdditions,
removals: totalRemovals, removals: totalRemovals,
title: `${filesToRead.length} files`,
}, },
output, output,
} }

View file

@ -16,14 +16,8 @@ export const ReadTool = Tool.define({
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
filePath: z.string().describe("The path to the file to read"), filePath: z.string().describe("The path to the file to read"),
offset: z offset: z.number().describe("The line number to start reading from (0-based)").optional(),
.number() limit: z.number().describe("The number of lines to read (defaults to 2000)").optional(),
.describe("The line number to start reading from (0-based)")
.optional(),
limit: z
.number()
.describe("The number of lines to read (defaults to 2000)")
.optional(),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
let filePath = params.filePath let filePath = params.filePath
@ -40,16 +34,13 @@ export const ReadTool = Tool.define({
const suggestions = dirEntries const suggestions = dirEntries
.filter( .filter(
(entry) => (entry) =>
entry.toLowerCase().includes(base.toLowerCase()) || entry.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(entry.toLowerCase()),
base.toLowerCase().includes(entry.toLowerCase()),
) )
.map((entry) => path.join(dir, entry)) .map((entry) => path.join(dir, entry))
.slice(0, 3) .slice(0, 3)
if (suggestions.length > 0) { if (suggestions.length > 0) {
throw new Error( throw new Error(`File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
`File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`,
)
} }
throw new Error(`File not found: ${filePath}`) throw new Error(`File not found: ${filePath}`)
@ -57,21 +48,14 @@ export const ReadTool = Tool.define({
const stats = await file.stat() const stats = await file.stat()
if (stats.size > MAX_READ_SIZE) if (stats.size > MAX_READ_SIZE)
throw new Error( throw new Error(`File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`)
`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)
if (isImage) if (isImage) throw new Error(`This is an image file of type: ${isImage}\nUse a different tool to process images`)
throw new Error(
`This is an image file of type: ${isImage}\nUse a different tool to process images`,
)
const lines = await file.text().then((text) => text.split("\n")) const lines = await file.text().then((text) => text.split("\n"))
const raw = lines.slice(offset, offset + limit).map((line) => { const raw = lines.slice(offset, offset + limit).map((line) => {
return line.length > MAX_LINE_LENGTH return line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line
? line.substring(0, MAX_LINE_LENGTH) + "..."
: line
}) })
const content = raw.map((line, index) => { const content = raw.map((line, index) => {
return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}` return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`
@ -82,9 +66,7 @@ export const ReadTool = Tool.define({
output += content.join("\n") output += content.join("\n")
if (lines.length > offset + content.length) { if (lines.length > offset + content.length) {
output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${ output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${offset + content.length})`
offset + content.length
})`
} }
output += "\n</file>" output += "\n</file>"
@ -93,10 +75,10 @@ export const ReadTool = Tool.define({
FileTime.read(ctx.sessionID, filePath) FileTime.read(ctx.sessionID, filePath)
return { return {
title: path.relative(App.info().path.root, filePath),
output, output,
metadata: { metadata: {
preview, preview,
title: path.relative(App.info().path.root, filePath),
}, },
} }
}, },

View file

@ -3,41 +3,36 @@ import DESCRIPTION from "./task.txt"
import { z } from "zod" import { z } from "zod"
import { Session } from "../session" import { Session } from "../session"
import { Bus } from "../bus" import { Bus } from "../bus"
import { Message } from "../session/message" import { MessageV2 } from "../session/message-v2"
export const TaskTool = Tool.define({ export const TaskTool = Tool.define({
id: "task", id: "task",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
description: z description: z.string().describe("A short (3-5 words) description of the task"),
.string()
.describe("A short (3-5 words) description of the task"),
prompt: z.string().describe("The task for the agent to perform"), prompt: z.string().describe("The task for the agent to perform"),
}), }),
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) const msg = (await Session.getMessage(ctx.sessionID, ctx.messageID)) as MessageV2.Assistant
const metadata = msg.metadata.assistant!
function summary(input: Message.Info) { function summary(input: MessageV2.Info) {
const result = [] const result = []
for (const part of input.parts) { for (const part of input.parts) {
if (part.type === "tool-invocation") { if (part.type === "tool" && part.state.status === "completed") {
result.push({ result.push(part)
toolInvocation: part.toolInvocation,
metadata: input.metadata.tool[part.toolInvocation.toolCallId],
})
} }
} }
return result return result
} }
const unsub = Bus.subscribe(Message.Event.Updated, async (evt) => { const unsub = Bus.subscribe(MessageV2.Event.Updated, async (evt) => {
if (evt.properties.info.metadata.sessionID !== session.id) return if (evt.properties.info.sessionID !== session.id) return
ctx.metadata({ ctx.metadata({
title: params.description, title: params.description,
metadata: {
summary: summary(evt.properties.info), summary: summary(evt.properties.info),
},
}) })
}) })
@ -46,8 +41,8 @@ export const TaskTool = Tool.define({
}) })
const result = await Session.chat({ const result = await Session.chat({
sessionID: session.id, sessionID: session.id,
modelID: metadata.modelID, modelID: msg.modelID,
providerID: metadata.providerID, providerID: msg.providerID,
parts: [ parts: [
{ {
type: "text", type: "text",
@ -57,8 +52,8 @@ export const TaskTool = Tool.define({
}) })
unsub() unsub()
return { return {
metadata: {
title: params.description, title: params.description,
metadata: {
summary: summary(result), summary: summary(result),
}, },
output: result.parts.findLast((x) => x.type === "text")!.text, output: result.parts.findLast((x) => x.type === "text")!.text,

View file

@ -5,12 +5,8 @@ import { App } from "../app/app"
const TodoInfo = z.object({ const TodoInfo = z.object({
content: z.string().min(1).describe("Brief description of the task"), content: z.string().min(1).describe("Brief description of the task"),
status: z status: z.enum(["pending", "in_progress", "completed"]).describe("Current status of the task"),
.enum(["pending", "in_progress", "completed"]) priority: z.enum(["high", "medium", "low"]).describe("Priority level of the task"),
.describe("Current status of the task"),
priority: z
.enum(["high", "medium", "low"])
.describe("Priority level of the task"),
id: z.string().describe("Unique identifier for the todo item"), id: z.string().describe("Unique identifier for the todo item"),
}) })
type TodoInfo = z.infer<typeof TodoInfo> type TodoInfo = z.infer<typeof TodoInfo>
@ -32,9 +28,9 @@ export const TodoWriteTool = Tool.define({
const todos = state() const todos = state()
todos[opts.sessionID] = params.todos todos[opts.sessionID] = params.todos
return { return {
title: `${params.todos.filter((x) => x.status !== "completed").length} todos`,
output: JSON.stringify(params.todos, null, 2), output: JSON.stringify(params.todos, null, 2),
metadata: { metadata: {
title: `${params.todos.filter((x) => x.status !== "completed").length} todos`,
todos: params.todos, todos: params.todos,
}, },
} }
@ -48,9 +44,9 @@ export const TodoReadTool = Tool.define({
async execute(_params, opts) { async execute(_params, opts) {
const todos = state()[opts.sessionID] ?? [] const todos = state()[opts.sessionID] ?? []
return { return {
title: `${todos.filter((x) => x.status !== "completed").length} todos`,
metadata: { metadata: {
todos, todos,
title: `${todos.filter((x) => x.status !== "completed").length} todos`,
}, },
output: JSON.stringify(todos, null, 2), output: JSON.stringify(todos, null, 2),
} }

View file

@ -2,19 +2,15 @@ import type { StandardSchemaV1 } from "@standard-schema/spec"
export namespace Tool { export namespace Tool {
interface Metadata { interface Metadata {
title: string
[key: string]: any [key: string]: any
} }
export type Context<M extends Metadata = Metadata> = { export type Context<M extends Metadata = Metadata> = {
sessionID: string sessionID: string
messageID: string messageID: string
abort: AbortSignal abort: AbortSignal
metadata(meta: M): void metadata(input: { title?: string; metadata?: M }): void
} }
export interface Info< export interface Info<Parameters extends StandardSchemaV1 = StandardSchemaV1, M extends Metadata = Metadata> {
Parameters extends StandardSchemaV1 = StandardSchemaV1,
M extends Metadata = Metadata,
> {
id: string id: string
description: string description: string
parameters: Parameters parameters: Parameters
@ -22,15 +18,15 @@ export namespace Tool {
args: StandardSchemaV1.InferOutput<Parameters>, args: StandardSchemaV1.InferOutput<Parameters>,
ctx: Context, ctx: Context,
): Promise<{ ): Promise<{
title: string
metadata: M metadata: M
output: string output: string
}> }>
} }
export function define< export function define<Parameters extends StandardSchemaV1, Result extends Metadata>(
Parameters extends StandardSchemaV1, input: Info<Parameters, Result>,
Result extends Metadata, ): Info<Parameters, Result> {
>(input: Info<Parameters, Result>): Info<Parameters, Result> {
return input return input
} }
} }

View file

@ -14,9 +14,7 @@ export const WebFetchTool = Tool.define({
url: z.string().describe("The URL to fetch content from"), url: z.string().describe("The URL to fetch content from"),
format: z format: z
.enum(["text", "markdown", "html"]) .enum(["text", "markdown", "html"])
.describe( .describe("The format to return the content in (text, markdown, or html)"),
"The format to return the content in (text, markdown, or html)",
),
timeout: z timeout: z
.number() .number()
.min(0) .min(0)
@ -26,17 +24,11 @@ export const WebFetchTool = Tool.define({
}), }),
async execute(params, ctx) { async execute(params, ctx) {
// Validate URL // Validate URL
if ( if (!params.url.startsWith("http://") && !params.url.startsWith("https://")) {
!params.url.startsWith("http://") &&
!params.url.startsWith("https://")
) {
throw new Error("URL must start with http:// or https://") throw new Error("URL must start with http:// or https://")
} }
const timeout = Math.min( const timeout = Math.min((params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000, MAX_TIMEOUT)
(params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000,
MAX_TIMEOUT,
)
const controller = new AbortController() const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout) const timeoutId = setTimeout(() => controller.abort(), timeout)
@ -46,8 +38,7 @@ export const WebFetchTool = Tool.define({
headers: { headers: {
"User-Agent": "User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
Accept: Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9", "Accept-Language": "en-US,en;q=0.9",
}, },
}) })
@ -79,16 +70,14 @@ export const WebFetchTool = Tool.define({
const text = await extractTextFromHTML(content) const text = await extractTextFromHTML(content)
return { return {
output: text, output: text,
metadata: {
title, title,
}, metadata: {},
} }
} }
return { return {
output: content, output: content,
metadata: {
title, title,
}, metadata: {},
} }
case "markdown": case "markdown":
@ -96,32 +85,28 @@ export const WebFetchTool = Tool.define({
const markdown = convertHTMLToMarkdown(content) const markdown = convertHTMLToMarkdown(content)
return { return {
output: markdown, output: markdown,
metadata: {
title, title,
}, metadata: {},
} }
} }
return { return {
output: "```\n" + content + "\n```", output: "```\n" + content + "\n```",
metadata: {
title, title,
}, metadata: {},
} }
case "html": case "html":
return { return {
output: content, output: content,
metadata: {
title, title,
}, metadata: {},
} }
default: default:
return { return {
output: content, output: content,
metadata: {
title, title,
}, metadata: {},
} }
} }
}, },
@ -143,16 +128,7 @@ async function extractTextFromHTML(html: string) {
.on("*", { .on("*", {
element(element) { element(element) {
// Reset skip flag when entering other elements // Reset skip flag when entering other elements
if ( if (!["script", "style", "noscript", "iframe", "object", "embed"].includes(element.tagName)) {
![
"script",
"style",
"noscript",
"iframe",
"object",
"embed",
].includes(element.tagName)
) {
skipContent = false skipContent = false
} }
}, },

View file

@ -13,18 +13,12 @@ export const WriteTool = Tool.define({
id: "write", id: "write",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
filePath: z filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"),
.string()
.describe(
"The absolute path to the file to write (must be absolute, not relative)",
),
content: z.string().describe("The content to write to the file"), content: z.string().describe("The content to write to the file"),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
const app = App.info() const app = App.info()
const filepath = path.isAbsolute(params.filePath) const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
? params.filePath
: path.join(app.path.cwd, params.filePath)
const file = Bun.file(filepath) const file = Bun.file(filepath)
const exists = await file.exists() const exists = await file.exists()
@ -33,9 +27,7 @@ export const WriteTool = Tool.define({
await Permission.ask({ await Permission.ask({
id: "write", id: "write",
sessionID: ctx.sessionID, sessionID: ctx.sessionID,
title: exists title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
? "Overwrite this file: " + filepath
: "Create new file: " + filepath,
metadata: { metadata: {
filePath: filepath, filePath: filepath,
content: params.content, content: params.content,
@ -62,11 +54,11 @@ export const WriteTool = Tool.define({
} }
return { return {
title: path.relative(app.path.root, filepath),
metadata: { metadata: {
diagnostics, diagnostics,
filepath, filepath,
exists: exists, exists: exists,
title: path.relative(app.path.root, filepath),
}, },
output, output,
} }

View file

@ -7,10 +7,7 @@ export abstract class NamedError extends Error {
abstract schema(): ZodSchema abstract schema(): ZodSchema
abstract toObject(): { name: string; data: any } abstract toObject(): { name: string; data: any }
static create<Name extends string, Data extends ZodSchema>( static create<Name extends string, Data extends ZodSchema>(name: Name, data: Data) {
name: Name,
data: Data,
) {
const schema = z const schema = z
.object({ .object({
name: z.literal(name), name: z.literal(name),

View file

@ -19,10 +19,7 @@ export namespace Log {
await fs.mkdir(dir, { recursive: true }) await fs.mkdir(dir, { recursive: true })
cleanup(dir) cleanup(dir)
if (options.print) return if (options.print) return
logpath = path.join( logpath = path.join(dir, new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log")
dir,
new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
)
const logfile = Bun.file(logpath) const logfile = Bun.file(logpath)
await fs.truncate(logpath).catch(() => {}) await fs.truncate(logpath).catch(() => {})
const writer = logfile.writer() const writer = logfile.writer()
@ -43,9 +40,7 @@ export namespace Log {
const filesToDelete = files.slice(0, -10) const filesToDelete = files.slice(0, -10)
await Promise.all( await Promise.all(filesToDelete.map((file) => fs.unlink(file).catch(() => {})))
filesToDelete.map((file) => fs.unlink(file).catch(() => {})),
)
} }
let last = Date.now() let last = Date.now()
@ -63,11 +58,7 @@ export namespace Log {
const next = new Date() const next = new Date()
const diff = next.getTime() - last const diff = next.getTime() - last
last = next.getTime() last = next.getTime()
return ( return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
[next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message]
.filter(Boolean)
.join(" ") + "\n"
)
} }
const result = { const result = {
info(message?: any, extra?: Record<string, any>) { info(message?: any, extra?: Record<string, any>) {

View file

@ -17,12 +17,7 @@ const testCases: TestCase[] = [
replace: 'console.log("universe");', replace: 'console.log("universe");',
}, },
{ {
content: [ content: ["if (condition) {", " doSomething();", " doSomethingElse();", "}"].join("\n"),
"if (condition) {",
" doSomething();",
" doSomethingElse();",
"}",
].join("\n"),
find: [" doSomething();", " doSomethingElse();"].join("\n"), find: [" doSomething();", " doSomethingElse();"].join("\n"),
replace: [" doNewThing();", " doAnotherThing();"].join("\n"), replace: [" doNewThing();", " doAnotherThing();"].join("\n"),
}, },
@ -53,15 +48,8 @@ const testCases: TestCase[] = [
" return result;", " return result;",
"}", "}",
].join("\n"), ].join("\n"),
find: [ find: ["function calculate(a, b) {", " // different middle content", " return result;", "}"].join("\n"),
"function calculate(a, b) {", replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join("\n"),
" // different middle content",
" return result;",
"}",
].join("\n"),
replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join(
"\n",
),
}, },
{ {
content: [ content: [
@ -76,13 +64,7 @@ const testCases: TestCase[] = [
"}", "}",
].join("\n"), ].join("\n"),
find: ["class MyClass {", " // different implementation", "}"].join("\n"), find: ["class MyClass {", " // different implementation", "}"].join("\n"),
replace: [ replace: ["class MyClass {", " constructor() {", " this.value = 42;", " }", "}"].join("\n"),
"class MyClass {",
" constructor() {",
" this.value = 42;",
" }",
"}",
].join("\n"),
}, },
// WhitespaceNormalizedReplacer cases // WhitespaceNormalizedReplacer cases
@ -104,48 +86,21 @@ const testCases: TestCase[] = [
// IndentationFlexibleReplacer cases // IndentationFlexibleReplacer cases
{ {
content: [ content: [" function nested() {", ' console.log("deeply nested");', " return true;", " }"].join(
" function nested() {", "\n",
' console.log("deeply nested");', ),
" return true;", find: ["function nested() {", ' console.log("deeply nested");', " return true;", "}"].join("\n"),
" }", replace: ["function nested() {", ' console.log("updated");', " return false;", "}"].join("\n"),
].join("\n"),
find: [
"function nested() {",
' console.log("deeply nested");',
" return true;",
"}",
].join("\n"),
replace: [
"function nested() {",
' console.log("updated");',
" return false;",
"}",
].join("\n"),
}, },
{ {
content: [ content: [" if (true) {", ' console.log("level 1");', ' console.log("level 2");', " }"].join("\n"),
" if (true) {", find: ["if (true) {", 'console.log("level 1");', ' console.log("level 2");', "}"].join("\n"),
' console.log("level 1");',
' console.log("level 2");',
" }",
].join("\n"),
find: [
"if (true) {",
'console.log("level 1");',
' console.log("level 2");',
"}",
].join("\n"),
replace: ["if (true) {", 'console.log("updated");', "}"].join("\n"), replace: ["if (true) {", 'console.log("updated");', "}"].join("\n"),
}, },
// replaceAll option cases // replaceAll option cases
{ {
content: [ content: ['console.log("test");', 'console.log("test");', 'console.log("test");'].join("\n"),
'console.log("test");',
'console.log("test");',
'console.log("test");',
].join("\n"),
find: 'console.log("test");', find: 'console.log("test");',
replace: 'console.log("updated");', replace: 'console.log("updated");',
all: true, all: true,
@ -213,9 +168,7 @@ const testCases: TestCase[] = [
// MultiOccurrenceReplacer cases (with replaceAll) // MultiOccurrenceReplacer cases (with replaceAll)
{ {
content: ["debug('start');", "debug('middle');", "debug('end');"].join( content: ["debug('start');", "debug('middle');", "debug('end');"].join("\n"),
"\n",
),
find: "debug", find: "debug",
replace: "log", replace: "log",
all: true, all: true,
@ -239,9 +192,7 @@ const testCases: TestCase[] = [
replace: "const value = 24;", replace: "const value = 24;",
}, },
{ {
content: ["", " if (condition) {", " doSomething();", " }", ""].join( content: ["", " if (condition) {", " doSomething();", " }", ""].join("\n"),
"\n",
),
find: ["if (condition) {", " doSomething();", "}"].join("\n"), find: ["if (condition) {", " doSomething();", "}"].join("\n"),
replace: ["if (condition) {", " doNothing();", "}"].join("\n"), replace: ["if (condition) {", " doNothing();", "}"].join("\n"),
}, },
@ -262,9 +213,7 @@ const testCases: TestCase[] = [
" return result;", " return result;",
"}", "}",
].join("\n"), ].join("\n"),
replace: ["function calculate(a, b) {", " return (a + b) * 2;", "}"].join( replace: ["function calculate(a, b) {", " return (a + b) * 2;", "}"].join("\n"),
"\n",
),
}, },
{ {
content: [ content: [
@ -278,15 +227,8 @@ const testCases: TestCase[] = [
" }", " }",
"}", "}",
].join("\n"), ].join("\n"),
find: [ find: ["class TestClass {", " // different implementation", " // with multiple lines", "}"].join("\n"),
"class TestClass {", replace: ["class TestClass {", " getValue() { return 42; }", "}"].join("\n"),
" // different implementation",
" // with multiple lines",
"}",
].join("\n"),
replace: ["class TestClass {", " getValue() { return 42; }", "}"].join(
"\n",
),
}, },
// Combined edge cases for new replacers // Combined edge cases for new replacers
@ -296,9 +238,7 @@ const testCases: TestCase[] = [
replace: 'console.log("updated");', replace: 'console.log("updated");',
}, },
{ {
content: [" ", "function test() {", " return 'value';", "}", " "].join( content: [" ", "function test() {", " return 'value';", "}", " "].join("\n"),
"\n",
),
find: ["function test() {", "return 'value';", "}"].join("\n"), find: ["function test() {", "return 'value';", "}"].join("\n"),
replace: ["function test() {", "return 'new value';", "}"].join("\n"), replace: ["function test() {", "return 'new value';", "}"].join("\n"),
}, },
@ -346,13 +286,7 @@ const testCases: TestCase[] = [
// ContextAwareReplacer - test with trailing newline in find string // ContextAwareReplacer - test with trailing newline in find string
{ {
content: [ content: ["class Test {", " method1() {", " return 1;", " }", "}"].join("\n"),
"class Test {",
" method1() {",
" return 1;",
" }",
"}",
].join("\n"),
find: [ find: [
"class Test {", "class Test {",
" // different content", " // different content",
@ -401,12 +335,7 @@ describe("EditTool Replacers", () => {
replace(testCase.content, testCase.find, testCase.replace, testCase.all) replace(testCase.content, testCase.find, testCase.replace, testCase.all)
}).toThrow() }).toThrow()
} else { } else {
const result = replace( const result = replace(testCase.content, testCase.find, testCase.replace, testCase.all)
testCase.content,
testCase.find,
testCase.replace,
testCase.all,
)
expect(result).toContain(testCase.replace) expect(result).toContain(testCase.replace)
} }
}) })

View file

@ -42,10 +42,7 @@ describe("tool.glob", () => {
describe("tool.ls", () => { describe("tool.ls", () => {
test("basic", async () => { test("basic", async () => {
const result = await App.provide({ cwd: process.cwd() }, async () => { const result = await App.provide({ cwd: process.cwd() }, async () => {
return await ListTool.execute( return await ListTool.execute({ path: "./example", ignore: [".git"] }, ctx)
{ path: "./example", ignore: [".git"] },
ctx,
)
}) })
expect(result.output).toMatchSnapshot() expect(result.output).toMatchSnapshot()
}) })

View file

@ -10,6 +10,7 @@ require (
github.com/charmbracelet/glamour v0.10.0 github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
github.com/charmbracelet/x/ansi v0.8.0 github.com/charmbracelet/x/ansi v0.8.0
github.com/google/uuid v1.6.0
github.com/lithammer/fuzzysearch v1.1.8 github.com/lithammer/fuzzysearch v1.1.8
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
github.com/muesli/reflow v0.3.0 github.com/muesli/reflow v0.3.0

View file

@ -92,6 +92,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=

View file

@ -30,7 +30,7 @@ type App struct {
Provider *opencode.Provider Provider *opencode.Provider
Model *opencode.Model Model *opencode.Model
Session *opencode.Session Session *opencode.Session
Messages []opencode.Message Messages []opencode.MessageUnion
Commands commands.CommandRegistry Commands commands.CommandRegistry
} }
@ -44,10 +44,10 @@ type SessionClearedMsg struct{}
type CompactSessionMsg struct{} type CompactSessionMsg struct{}
type SendMsg struct { type SendMsg struct {
Text string Text string
Attachments []Attachment Attachments []opencode.FilePartParam
} }
type OptimisticMessageAddedMsg struct { type OptimisticMessageAddedMsg struct {
Message opencode.Message Message opencode.MessageUnion
} }
type FileRenderedMsg struct { type FileRenderedMsg struct {
FilePath string FilePath string
@ -116,7 +116,7 @@ func New(
State: appState, State: appState,
Client: httpClient, Client: httpClient,
Session: &opencode.Session{}, Session: &opencode.Session{},
Messages: []opencode.Message{}, Messages: []opencode.MessageUnion{},
Commands: commands.LoadFromConfig(configInfo), Commands: commands.LoadFromConfig(configInfo),
} }
@ -217,20 +217,16 @@ func getDefaultModel(
return nil return nil
} }
type Attachment struct {
FilePath string
FileName string
MimeType string
Content []byte
}
func (a *App) IsBusy() bool { func (a *App) IsBusy() bool {
if len(a.Messages) == 0 { if len(a.Messages) == 0 {
return false return false
} }
lastMessage := a.Messages[len(a.Messages)-1] lastMessage := a.Messages[len(a.Messages)-1]
return lastMessage.Metadata.Time.Completed == 0 if casted, ok := lastMessage.(opencode.AssistantMessage); ok {
return casted.Time.Completed == 0
}
return false
} }
func (a *App) SaveState() { func (a *App) SaveState() {
@ -296,43 +292,69 @@ func (a *App) CreateSession(ctx context.Context) (*opencode.Session, error) {
return session, nil return session, nil
} }
func (a *App) SendChatMessage(ctx context.Context, text string, attachments []Attachment) tea.Cmd { func (a *App) SendChatMessage(
ctx context.Context,
text string,
attachments []opencode.FilePartParam,
) (*App, tea.Cmd) {
var cmds []tea.Cmd var cmds []tea.Cmd
if a.Session.ID == "" { if a.Session.ID == "" {
session, err := a.CreateSession(ctx) session, err := a.CreateSession(ctx)
if err != nil { if err != nil {
return 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(SessionSelectedMsg(session)))
} }
optimisticMessage := opencode.Message{ optimisticParts := []opencode.UserMessagePart{{
ID: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()), Type: opencode.UserMessagePartTypeText,
Role: opencode.MessageRoleUser,
Parts: []opencode.MessagePart{{
Type: opencode.MessagePartTypeText,
Text: text, Text: text,
}}, }}
Metadata: opencode.MessageMetadata{ if len(attachments) > 0 {
for _, attachment := range attachments {
optimisticParts = append(optimisticParts, opencode.UserMessagePart{
Type: opencode.UserMessagePartTypeFile,
Filename: attachment.Filename.Value,
Mime: attachment.Mime.Value,
URL: attachment.URL.Value,
})
}
}
optimisticMessage := opencode.UserMessage{
ID: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
Role: opencode.UserMessageRoleUser,
Parts: optimisticParts,
SessionID: a.Session.ID, SessionID: a.Session.ID,
Time: opencode.MessageMetadataTime{ Time: opencode.UserMessageTime{
Created: float64(time.Now().Unix()), Created: float64(time.Now().Unix()),
}, },
},
} }
a.Messages = append(a.Messages, optimisticMessage) a.Messages = append(a.Messages, optimisticMessage)
cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage})) cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage}))
cmds = append(cmds, func() tea.Msg { cmds = append(cmds, func() tea.Msg {
_, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{ parts := []opencode.UserMessagePartUnionParam{
Parts: opencode.F([]opencode.MessagePartUnionParam{
opencode.TextPartParam{ opencode.TextPartParam{
Type: opencode.F(opencode.TextPartTypeText), Type: opencode.F(opencode.TextPartTypeText),
Text: opencode.F(text), Text: opencode.F(text),
}, },
}), }
if len(attachments) > 0 {
for _, attachment := range attachments {
parts = append(parts, opencode.FilePartParam{
Mime: attachment.Mime,
Type: attachment.Type,
URL: attachment.URL,
Filename: attachment.Filename,
})
}
}
_, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{
Parts: opencode.F(parts),
ProviderID: opencode.F(a.Provider.ID), ProviderID: opencode.F(a.Provider.ID),
ModelID: opencode.F(a.Model.ID), ModelID: opencode.F(a.Model.ID),
}) })
@ -346,7 +368,7 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
// The actual response will come through SSE // The actual response will come through SSE
// For now, just return success // For now, just return success
return tea.Batch(cmds...) return a, tea.Batch(cmds...)
} }
func (a *App) Cancel(ctx context.Context, sessionID string) error { func (a *App) Cancel(ctx context.Context, sessionID string) error {

View file

@ -16,12 +16,11 @@ import (
type filesAndFoldersContextGroup struct { type filesAndFoldersContextGroup struct {
app *app.App app *app.App
prefix string
gitFiles []dialog.CompletionItemI gitFiles []dialog.CompletionItemI
} }
func (cg *filesAndFoldersContextGroup) GetId() string { func (cg *filesAndFoldersContextGroup) GetId() string {
return cg.prefix return "files"
} }
func (cg *filesAndFoldersContextGroup) GetEmptyMessage() string { func (cg *filesAndFoldersContextGroup) GetEmptyMessage() string {
@ -108,8 +107,9 @@ func (cg *filesAndFoldersContextGroup) GetChildEntries(
func NewFileAndFolderContextGroup(app *app.App) dialog.CompletionProvider { func NewFileAndFolderContextGroup(app *app.App) dialog.CompletionProvider {
cg := &filesAndFoldersContextGroup{ cg := &filesAndFoldersContextGroup{
app: app, app: app,
prefix: "file",
} }
go func() {
cg.gitFiles = cg.getGitFiles() cg.gitFiles = cg.getGitFiles()
}()
return cg return cg
} }

View file

@ -3,11 +3,14 @@ package chat
import ( import (
"fmt" "fmt"
"log/slog" "log/slog"
"path/filepath"
"strings" "strings"
"github.com/charmbracelet/bubbles/v2/spinner" "github.com/charmbracelet/bubbles/v2/spinner"
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/lipgloss/v2"
"github.com/google/uuid"
"github.com/sst/opencode-sdk-go"
"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/components/dialog"
@ -37,7 +40,6 @@ type EditorComponent interface {
type editorComponent struct { type editorComponent struct {
app *app.App app *app.App
textarea textarea.Model textarea textarea.Model
attachments []app.Attachment
spinner spinner.Model spinner spinner.Model
interruptKeyInDebounce bool interruptKeyInDebounce bool
} }
@ -66,17 +68,54 @@ 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:
if msg.IsCommand { switch msg.ProviderID {
case "commands":
commandName := strings.TrimPrefix(msg.CompletionValue, "/") commandName := strings.TrimPrefix(msg.CompletionValue, "/")
updated, cmd := m.Clear() updated, cmd := m.Clear()
m = updated.(*editorComponent) m = updated.(*editorComponent)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
cmds = append(cmds, util.CmdHandler(commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)]))) cmds = append(cmds, util.CmdHandler(commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)])))
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
} else { case "files":
existingValue := m.textarea.Value() atIndex := m.textarea.LastRuneIndex('@')
if atIndex == -1 {
// Should not happen, but as a fallback, just insert.
m.textarea.InsertString(msg.CompletionValue + " ")
return m, nil
}
// Replace the current token (after last space) // The range to replace is from the '@' up to the current cursor position.
// Replace the search term (e.g., "@search") with an empty string first.
cursorCol := m.textarea.CursorColumn()
m.textarea.ReplaceRange(atIndex, cursorCol, "")
// Now, insert the attachment at the position where the '@' was.
// The cursor is now at `atIndex` after the replacement.
filePath := msg.CompletionValue
extension := filepath.Ext(filePath)
mediaType := ""
switch extension {
case ".jpg":
mediaType = "image/jpeg"
case ".png", ".jpeg", ".gif", ".webp":
mediaType = "image/" + extension[1:]
case ".pdf":
mediaType = "application/pdf"
default:
mediaType = "text/plain"
}
attachment := &textarea.Attachment{
ID: uuid.NewString(),
Display: "@" + filePath,
URL: fmt.Sprintf("file://./%s", filePath),
Filename: filePath,
MediaType: mediaType,
}
m.textarea.InsertAttachment(attachment)
m.textarea.InsertString(" ")
return m, nil
default:
existingValue := m.textarea.Value()
lastSpaceIndex := strings.LastIndex(existingValue, " ") lastSpaceIndex := strings.LastIndex(existingValue, " ")
if lastSpaceIndex == -1 { if lastSpaceIndex == -1 {
m.textarea.SetValue(msg.CompletionValue + " ") m.textarea.SetValue(msg.CompletionValue + " ")
@ -128,7 +167,15 @@ func (m *editorComponent) Content(width int) string {
if m.app.IsBusy() { if m.app.IsBusy() {
keyText := m.getInterruptKeyText() keyText := m.getInterruptKeyText()
if m.interruptKeyInDebounce { if m.interruptKeyInDebounce {
hint = muted("working") + m.spinner.View() + muted(" ") + base(keyText+" again") + muted(" interrupt") hint = muted(
"working",
) + m.spinner.View() + muted(
" ",
) + base(
keyText+" again",
) + muted(
" interrupt",
)
} else { } else {
hint = muted("working") + m.spinner.View() + muted(" ") + base(keyText) + muted(" interrupt") hint = muted("working") + m.spinner.View() + muted(" ") + base(keyText) + muted(" interrupt")
} }
@ -190,19 +237,29 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
} }
if len(value) > 0 && value[len(value)-1] == '\\' { if len(value) > 0 && value[len(value)-1] == '\\' {
// If the last character is a backslash, remove it and add a newline // If the last character is a backslash, remove it and add a newline
m.textarea.SetValue(value[:len(value)-1] + "\n") m.textarea.ReplaceRange(len(value)-1, len(value), "")
m.textarea.InsertString("\n")
return m, nil return m, nil
} }
var cmds []tea.Cmd var cmds []tea.Cmd
attachments := m.textarea.GetAttachments()
fileParts := make([]opencode.FilePartParam, 0)
for _, attachment := range attachments {
fileParts = append(fileParts, opencode.FilePartParam{
Type: opencode.F(opencode.FilePartTypeFile),
Mime: opencode.F(attachment.MediaType),
URL: opencode.F(attachment.URL),
Filename: opencode.F(attachment.Filename),
})
}
updated, cmd := m.Clear() updated, cmd := m.Clear()
m = updated.(*editorComponent) m = updated.(*editorComponent)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
attachments := m.attachments cmds = append(cmds, util.CmdHandler(app.SendMsg{Text: value, Attachments: fileParts}))
m.attachments = nil
cmds = append(cmds, util.CmdHandler(app.SendMsg{Text: value, Attachments: attachments}))
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
} }
@ -212,18 +269,23 @@ func (m *editorComponent) Clear() (tea.Model, tea.Cmd) {
} }
func (m *editorComponent) Paste() (tea.Model, tea.Cmd) { func (m *editorComponent) Paste() (tea.Model, tea.Cmd) {
imageBytes, text, err := image.GetImageFromClipboard() _, text, err := image.GetImageFromClipboard()
if err != nil { if err != nil {
slog.Error(err.Error()) slog.Error(err.Error())
return m, nil return m, nil
} }
if len(imageBytes) != 0 { // if len(imageBytes) != 0 {
attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments)) // attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments))
attachment := app.Attachment{FilePath: attachmentName, FileName: attachmentName, Content: imageBytes, MimeType: "image/png"} // attachment := app.Attachment{
m.attachments = append(m.attachments, attachment) // FilePath: attachmentName,
} else { // FileName: attachmentName,
m.textarea.SetValue(m.textarea.Value() + text) // Content: imageBytes,
} // MimeType: "image/png",
// }
// m.attachments = append(m.attachments, attachment)
// } else {
m.textarea.InsertString(text)
// }
return m, nil return m, nil
} }
@ -254,12 +316,26 @@ func createTextArea(existing *textarea.Model) textarea.Model {
ta.Styles.Blurred.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss() ta.Styles.Blurred.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
ta.Styles.Blurred.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss() ta.Styles.Blurred.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
ta.Styles.Blurred.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss() ta.Styles.Blurred.Placeholder = styles.NewStyle().
Foreground(textMutedColor).
Background(bgColor).
Lipgloss()
ta.Styles.Blurred.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss() ta.Styles.Blurred.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
ta.Styles.Focused.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss() ta.Styles.Focused.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
ta.Styles.Focused.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss() ta.Styles.Focused.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
ta.Styles.Focused.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss() ta.Styles.Focused.Placeholder = styles.NewStyle().
Foreground(textMutedColor).
Background(bgColor).
Lipgloss()
ta.Styles.Focused.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss() ta.Styles.Focused.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
ta.Styles.Attachment = styles.NewStyle().
Foreground(t.Secondary()).
Background(bgColor).
Lipgloss()
ta.Styles.SelectedAttachment = styles.NewStyle().
Foreground(t.Text()).
Background(t.Secondary()).
Lipgloss()
ta.Styles.Cursor.Color = t.Primary() ta.Styles.Cursor.Color = t.Primary()
ta.Prompt = " " ta.Prompt = " "

View file

@ -17,7 +17,6 @@ import (
"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"
"github.com/tidwall/gjson"
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
) )
@ -217,17 +216,35 @@ func renderContentBlock(
func renderText( func renderText(
app *app.App, app *app.App,
message opencode.Message, message opencode.MessageUnion,
text string, text string,
author string, author string,
showToolDetails bool, showToolDetails bool,
highlight bool, highlight bool,
width int, width int,
toolCalls ...opencode.ToolInvocationPart, extra string,
toolCalls ...opencode.ToolPart,
) string { ) string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)). var ts time.Time
backgroundColor := t.BackgroundPanel()
if highlight {
backgroundColor = t.BackgroundElement()
}
messageStyle := styles.NewStyle().Background(backgroundColor)
content := messageStyle.Render(text)
switch casted := message.(type) {
case opencode.AssistantMessage:
ts = time.UnixMilli(int64(casted.Time.Created))
content = util.ToMarkdown(text, width, backgroundColor)
case opencode.UserMessage:
ts = time.UnixMilli(int64(casted.Time.Created))
messageStyle = messageStyle.Width(width - 6)
}
timestamp := ts.
Local(). Local().
Format("02 Jan 2006 03:04 PM") Format("02 Jan 2006 03:04 PM")
if time.Now().Format("02 Jan 2006") == timestamp[:11] { if time.Now().Format("02 Jan 2006") == timestamp[:11] {
@ -237,30 +254,12 @@ func renderText(
info := fmt.Sprintf("%s (%s)", author, timestamp) info := fmt.Sprintf("%s (%s)", author, timestamp)
info = styles.NewStyle().Foreground(t.TextMuted()).Render(info) info = styles.NewStyle().Foreground(t.TextMuted()).Render(info)
backgroundColor := t.BackgroundPanel()
if highlight {
backgroundColor = t.BackgroundElement()
}
messageStyle := styles.NewStyle().Background(backgroundColor)
if message.Role == opencode.MessageRoleUser {
messageStyle = messageStyle.Width(width - 6)
}
content := messageStyle.Render(text)
if message.Role == opencode.MessageRoleAssistant {
content = util.ToMarkdown(text, width, backgroundColor)
}
if !showToolDetails && toolCalls != nil && len(toolCalls) > 0 { if !showToolDetails && toolCalls != nil && len(toolCalls) > 0 {
content = content + "\n\n" content = content + "\n\n"
for _, toolCall := range toolCalls { for _, toolCall := range toolCalls {
title := renderToolTitle(toolCall, message.Metadata, width) title := renderToolTitle(toolCall, width)
metadata := opencode.MessageMetadataTool{}
if _, ok := message.Metadata.Tool[toolCall.ToolInvocation.ToolCallID]; ok {
metadata = message.Metadata.Tool[toolCall.ToolInvocation.ToolCallID]
}
style := styles.NewStyle() style := styles.NewStyle()
if _, ok := metadata.ExtraFields["error"]; ok { if toolCall.State.Status == opencode.ToolPartStateStatusError {
style = style.Foreground(t.Error()) style = style.Foreground(t.Error())
} }
title = style.Render(title) title = style.Render(title)
@ -269,10 +268,14 @@ func renderText(
} }
} }
content = strings.Join([]string{content, info}, "\n") sections := []string{content, info}
if extra != "" {
sections = append(sections, "\n"+extra)
}
content = strings.Join(sections, "\n")
switch message.Role { switch message.(type) {
case opencode.MessageRoleUser: case opencode.UserMessage:
return renderContentBlock( return renderContentBlock(
app, app,
content, content,
@ -281,7 +284,7 @@ func renderText(
WithTextColor(t.Text()), WithTextColor(t.Text()),
WithBorderColorRight(t.Secondary()), WithBorderColorRight(t.Secondary()),
) )
case opencode.MessageRoleAssistant: case opencode.AssistantMessage:
return renderContentBlock( return renderContentBlock(
app, app,
content, content,
@ -295,39 +298,32 @@ func renderText(
func renderToolDetails( func renderToolDetails(
app *app.App, app *app.App,
toolCall opencode.ToolInvocationPart, toolCall opencode.ToolPart,
messageMetadata opencode.MessageMetadata,
highlight bool, highlight bool,
width int, width int,
) string { ) string {
ignoredTools := []string{"todoread"} ignoredTools := []string{"todoread"}
if slices.Contains(ignoredTools, toolCall.ToolInvocation.ToolName) { if slices.Contains(ignoredTools, toolCall.Tool) {
return "" return ""
} }
toolCallID := toolCall.ToolInvocation.ToolCallID if toolCall.State.Status == opencode.ToolPartStateStatusPending || toolCall.State.Status == opencode.ToolPartStateStatusRunning {
metadata := opencode.MessageMetadataTool{} title := renderToolTitle(toolCall, width)
if _, ok := messageMetadata.Tool[toolCallID]; ok {
metadata = messageMetadata.Tool[toolCallID]
}
var result *string
if toolCall.ToolInvocation.Result != "" {
result = &toolCall.ToolInvocation.Result
}
if toolCall.ToolInvocation.State == "partial-call" {
title := renderToolTitle(toolCall, messageMetadata, width)
return renderContentBlock(app, title, highlight, width) return renderContentBlock(app, title, highlight, width)
} }
toolArgsMap := make(map[string]any) var result *string
if toolCall.ToolInvocation.Args != nil { if toolCall.State.Output != "" {
value := toolCall.ToolInvocation.Args result = &toolCall.State.Output
}
toolInputMap := make(map[string]any)
if toolCall.State.Input != nil {
value := toolCall.State.Input
if m, ok := value.(map[string]any); ok { if m, ok := value.(map[string]any); ok {
toolArgsMap = m toolInputMap = m
keys := make([]string, 0, len(toolArgsMap)) keys := make([]string, 0, len(toolInputMap))
for key := range toolArgsMap { for key := range toolInputMap {
keys = append(keys, key) keys = append(keys, key)
} }
slices.Sort(keys) slices.Sort(keys)
@ -335,7 +331,6 @@ func renderToolDetails(
} }
body := "" body := ""
finished := result != nil && *result != ""
t := theme.CurrentTheme() t := theme.CurrentTheme()
backgroundColor := t.BackgroundPanel() backgroundColor := t.BackgroundPanel()
borderColor := t.BackgroundPanel() borderColor := t.BackgroundPanel()
@ -344,17 +339,19 @@ func renderToolDetails(
borderColor = t.BorderActive() borderColor = t.BorderActive()
} }
switch toolCall.ToolInvocation.ToolName { if toolCall.State.Status == opencode.ToolPartStateStatusCompleted {
metadata := toolCall.State.Metadata.(map[string]any)
switch toolCall.Tool {
case "read": case "read":
preview := metadata.ExtraFields["preview"] preview := metadata["preview"]
if preview != nil && toolArgsMap["filePath"] != nil { if preview != nil && toolInputMap["filePath"] != nil {
filename := toolArgsMap["filePath"].(string) filename := toolInputMap["filePath"].(string)
body = preview.(string) body = preview.(string)
body = util.RenderFile(filename, body, width, util.WithTruncate(6)) body = util.RenderFile(filename, body, width, util.WithTruncate(6))
} }
case "edit": case "edit":
if filename, ok := toolArgsMap["filePath"].(string); ok { if filename, ok := toolInputMap["filePath"].(string); ok {
diffField := metadata.ExtraFields["diff"] diffField := metadata["diff"]
if diffField != nil { if diffField != nil {
patch := diffField.(string) patch := diffField.(string)
var formattedDiff string var formattedDiff string
@ -378,7 +375,7 @@ func renderToolDetails(
body += "\n" + diagnostics body += "\n" + diagnostics
} }
title := renderToolTitle(toolCall, messageMetadata, width) title := renderToolTitle(toolCall, width)
title = style.Render(title) title = style.Render(title)
content := title + "\n" + body content := title + "\n" + body
content = renderContentBlock( content = renderContentBlock(
@ -393,8 +390,8 @@ func renderToolDetails(
} }
} }
case "write": case "write":
if filename, ok := toolArgsMap["filePath"].(string); ok { if filename, ok := toolInputMap["filePath"].(string); ok {
if content, ok := toolArgsMap["content"].(string); ok { if content, ok := toolInputMap["content"].(string); ok {
body = util.RenderFile(filename, content, width) body = util.RenderFile(filename, content, width)
if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" { if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
body += "\n\n" + diagnostics body += "\n\n" + diagnostics
@ -402,14 +399,14 @@ func renderToolDetails(
} }
} }
case "bash": case "bash":
stdout := metadata.ExtraFields["stdout"] stdout := metadata["stdout"]
if stdout != nil { if stdout != nil {
command := toolArgsMap["command"].(string) command := toolInputMap["command"].(string)
body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout) body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
body = util.ToMarkdown(body, width, backgroundColor) body = util.ToMarkdown(body, width, backgroundColor)
} }
case "webfetch": case "webfetch":
if format, ok := toolArgsMap["format"].(string); ok && result != nil { if format, ok := toolInputMap["format"].(string); ok && result != nil {
body = *result body = *result
body = util.TruncateHeight(body, 10) body = util.TruncateHeight(body, 10)
if format == "html" || format == "markdown" { if format == "html" || format == "markdown" {
@ -417,13 +414,12 @@ func renderToolDetails(
} }
} }
case "todowrite": case "todowrite":
todos := metadata.JSON.ExtraFields["todos"] todos := metadata["todos"]
if !todos.IsNull() && finished { if todos != nil {
strTodos := todos.Raw() for _, item := range todos.([]any) {
todos := gjson.Parse(strTodos) todo := item.(map[string]any)
for _, todo := range todos.Array() { content := todo["content"].(string)
content := todo.Get("content").String() switch todo["status"] {
switch todo.Get("status").String() {
case "completed": case "completed":
body += fmt.Sprintf("- [x] %s\n", content) body += fmt.Sprintf("- [x] %s\n", content)
// case "in-progress": // case "in-progress":
@ -435,30 +431,21 @@ func renderToolDetails(
body = util.ToMarkdown(body, width, backgroundColor) body = util.ToMarkdown(body, width, backgroundColor)
} }
case "task": case "task":
summary := metadata.JSON.ExtraFields["summary"] summary := metadata["summary"]
if !summary.IsNull() { if summary != nil {
strValue := summary.Raw() toolcalls := summary.([]any)
toolcalls := gjson.Parse(strValue).Array()
steps := []string{} steps := []string{}
for _, toolcall := range toolcalls { for _, toolcall := range toolcalls {
call := toolcall.Value().(map[string]any) call := toolcall.(map[string]any)
if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok { if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok {
data, _ := json.Marshal(toolInvocation) data, _ := json.Marshal(toolInvocation)
var toolCall opencode.ToolInvocationPart var toolCall opencode.ToolPart
_ = json.Unmarshal(data, &toolCall) _ = json.Unmarshal(data, &toolCall)
step := renderToolTitle(toolCall, width)
if metadata, ok := call["metadata"].(map[string]any); ok {
data, _ = json.Marshal(metadata)
var toolMetadata opencode.MessageMetadataTool
_ = json.Unmarshal(data, &toolMetadata)
step := renderToolTitle(toolCall, messageMetadata, width)
step = "∟ " + step step = "∟ " + step
steps = append(steps, step) steps = append(steps, step)
} }
} }
}
body = strings.Join(steps, "\n") body = strings.Join(steps, "\n")
} }
default: default:
@ -469,12 +456,11 @@ func renderToolDetails(
body = *result body = *result
body = util.TruncateHeight(body, 10) body = util.TruncateHeight(body, 10)
} }
}
error := "" error := ""
if err, ok := metadata.ExtraFields["error"].(bool); ok && err { if toolCall.State.Status == opencode.ToolPartStateStatusError {
if message, ok := metadata.ExtraFields["message"].(string); ok { error = toolCall.State.Error
error = message
}
} }
if error != "" { if error != "" {
@ -489,7 +475,7 @@ func renderToolDetails(
body = util.TruncateHeight(body, 10) body = util.TruncateHeight(body, 10)
} }
title := renderToolTitle(toolCall, messageMetadata, width) title := renderToolTitle(toolCall, width)
content := title + "\n\n" + body content := title + "\n\n" + body
return renderContentBlock(app, content, highlight, width, WithBorderColor(borderColor)) return renderContentBlock(app, content, highlight, width, WithBorderColor(borderColor))
} }
@ -510,20 +496,19 @@ func renderToolName(name string) string {
} }
func renderToolTitle( func renderToolTitle(
toolCall opencode.ToolInvocationPart, toolCall opencode.ToolPart,
messageMetadata opencode.MessageMetadata,
width int, width int,
) string { ) string {
// TODO: handle truncate to width // TODO: handle truncate to width
if toolCall.ToolInvocation.State == "partial-call" { if toolCall.State.Status == opencode.ToolPartStateStatusPending {
return renderToolAction(toolCall.ToolInvocation.ToolName) return renderToolAction(toolCall.Tool)
} }
toolArgs := "" toolArgs := ""
toolArgsMap := make(map[string]any) toolArgsMap := make(map[string]any)
if toolCall.ToolInvocation.Args != nil { if toolCall.State.Input != nil {
value := toolCall.ToolInvocation.Args value := toolCall.State.Input
if m, ok := value.(map[string]any); ok { if m, ok := value.(map[string]any); ok {
toolArgsMap = m toolArgsMap = m
@ -541,8 +526,8 @@ func renderToolTitle(
} }
} }
title := renderToolName(toolCall.ToolInvocation.ToolName) title := renderToolName(toolCall.Tool)
switch toolCall.ToolInvocation.ToolName { switch toolCall.Tool {
case "read": case "read":
toolArgs = renderArgs(&toolArgsMap, "filePath") toolArgs = renderArgs(&toolArgsMap, "filePath")
title = fmt.Sprintf("%s %s", title, toolArgs) title = fmt.Sprintf("%s %s", title, toolArgs)
@ -560,7 +545,7 @@ func renderToolTitle(
case "todowrite", "todoread": case "todowrite", "todoread":
// title is just the tool name // title is just the tool name
default: default:
toolName := renderToolName(toolCall.ToolInvocation.ToolName) toolName := renderToolName(toolCall.Tool)
title = fmt.Sprintf("%s %s", toolName, toolArgs) title = fmt.Sprintf("%s %s", toolName, toolArgs)
} }
return title return title
@ -640,8 +625,8 @@ type Diagnostic struct {
} }
// renderDiagnostics formats LSP diagnostics for display in the TUI // renderDiagnostics formats LSP diagnostics for display in the TUI
func renderDiagnostics(metadata opencode.MessageMetadataTool, filePath string) string { func renderDiagnostics(metadata map[string]any, filePath string) string {
if diagnosticsData, ok := metadata.ExtraFields["diagnostics"].(map[string]any); ok { if diagnosticsData, ok := metadata["diagnostics"].(map[string]any); ok {
if fileDiagnostics, ok := diagnosticsData[filePath].([]any); ok { if fileDiagnostics, ok := diagnosticsData[filePath].([]any); ok {
var errorDiagnostics []string var errorDiagnostics []string
for _, diagInterface := range fileDiagnostics { for _, diagInterface := range fileDiagnostics {

View file

@ -9,6 +9,7 @@ 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/components/dialog"
"github.com/sst/opencode/internal/layout"
"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"
@ -67,11 +68,9 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.selectedPart = -1 m.selectedPart = -1
return m, nil return m, nil
case app.OptimisticMessageAddedMsg: case app.OptimisticMessageAddedMsg:
m.renderView(m.width) m.tail = true
if m.tail { m.rendering = true
m.viewport.GotoBottom() return m, m.Reload()
}
return m, nil
case dialog.ThemeSelectedMsg: case dialog.ThemeSelectedMsg:
m.cache.Clear() m.cache.Clear()
m.rendering = true m.rendering = true
@ -100,7 +99,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
} }
case opencode.EventListResponseEventMessageUpdated: case opencode.EventListResponseEventMessageUpdated:
if msg.Properties.Info.Metadata.SessionID == m.app.Session.ID { if msg.Properties.Info.SessionID == m.app.Session.ID {
m.renderView(m.width) m.renderView(m.width)
if m.tail { if m.tail {
m.viewport.GotoBottom() m.viewport.GotoBottom()
@ -125,18 +124,58 @@ func (m *messagesComponent) renderView(width int) {
m.partCount = 0 m.partCount = 0
m.lineCount = 0 m.lineCount = 0
orphanedToolCalls := make([]opencode.ToolInvocationPart, 0) orphanedToolCalls := make([]opencode.ToolPart, 0)
for _, message := range m.app.Messages { for _, message := range m.app.Messages {
var content string var content string
var cached bool var cached bool
switch message.Role { switch casted := message.(type) {
case opencode.MessageRoleUser: case opencode.UserMessage:
for _, part := range message.Parts { userLoop:
for partIndex, part := range casted.Parts {
switch part := part.AsUnion().(type) { switch part := part.AsUnion().(type) {
case opencode.TextPart: case opencode.TextPart:
key := m.cache.GenerateKey(message.ID, part.Text, width, m.selectedPart == m.partCount) remainingParts := casted.Parts[partIndex+1:]
fileParts := make([]opencode.FilePart, 0)
for _, part := range remainingParts {
switch part := part.AsUnion().(type) {
case opencode.FilePart:
fileParts = append(fileParts, part)
}
}
flexItems := []layout.FlexItem{}
if len(fileParts) > 0 {
fileStyle := styles.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Padding(0, 1)
mediaTypeStyle := styles.NewStyle().Background(t.Secondary()).Foreground(t.BackgroundPanel()).Padding(0, 1)
for _, filePart := range fileParts {
mediaType := ""
switch filePart.Mime {
case "text/plain":
mediaType = "txt"
case "image/png", "image/jpeg", "image/gif", "image/webp":
mediaType = "img"
mediaTypeStyle = mediaTypeStyle.Background(t.Accent())
case "application/pdf":
mediaType = "pdf"
mediaTypeStyle = mediaTypeStyle.Background(t.Primary())
}
flexItems = append(flexItems, layout.FlexItem{
View: mediaTypeStyle.Render(mediaType) + fileStyle.Render(filePart.Filename),
})
}
}
bgColor := t.BackgroundPanel()
files := layout.Render(
layout.FlexOptions{
Background: &bgColor,
Width: width - 6,
Direction: layout.Column,
},
flexItems...,
)
key := m.cache.GenerateKey(casted.ID, part.Text, width, m.selectedPart == m.partCount, files)
content, cached = m.cache.Get(key) content, cached = m.cache.Get(key)
if !cached { if !cached {
content = renderText( content = renderText(
@ -147,6 +186,7 @@ func (m *messagesComponent) renderView(width int) {
m.showToolDetails, m.showToolDetails,
m.partCount == m.selectedPart, m.partCount == m.selectedPart,
width, width,
files,
) )
m.cache.Set(key, content) m.cache.Set(key, content)
} }
@ -154,24 +194,26 @@ func (m *messagesComponent) renderView(width int) {
m = m.updateSelected(content, part.Text) m = m.updateSelected(content, part.Text)
blocks = append(blocks, content) blocks = append(blocks, content)
} }
// Only render the first text part
break userLoop
} }
} }
case opencode.MessageRoleAssistant: case opencode.AssistantMessage:
hasTextPart := false hasTextPart := false
for partIndex, p := range message.Parts { for partIndex, p := range casted.Parts {
switch part := p.AsUnion().(type) { switch part := p.AsUnion().(type) {
case opencode.TextPart: case opencode.TextPart:
hasTextPart = true hasTextPart = true
finished := message.Metadata.Time.Completed > 0 finished := casted.Time.Completed > 0
remainingParts := message.Parts[partIndex+1:] remainingParts := casted.Parts[partIndex+1:]
toolCallParts := make([]opencode.ToolInvocationPart, 0) toolCallParts := make([]opencode.ToolPart, 0)
// sometimes tool calls happen without an assistant message // sometimes tool calls happen without an assistant message
// these should be included in this assistant message as well // these should be included in this assistant message as well
if len(orphanedToolCalls) > 0 { if len(orphanedToolCalls) > 0 {
toolCallParts = append(toolCallParts, orphanedToolCalls...) toolCallParts = append(toolCallParts, orphanedToolCalls...)
orphanedToolCalls = make([]opencode.ToolInvocationPart, 0) orphanedToolCalls = make([]opencode.ToolPart, 0)
} }
remaining := true remaining := true
@ -184,9 +226,9 @@ func (m *messagesComponent) renderView(width int) {
// we only want tool calls associated with the current text part. // we only want tool calls associated with the current text part.
// if we hit another text part, we're done. // if we hit another text part, we're done.
remaining = false remaining = false
case opencode.ToolInvocationPart: case opencode.ToolPart:
toolCallParts = append(toolCallParts, part) toolCallParts = append(toolCallParts, part)
if part.ToolInvocation.State != "result" { if part.State.Status != opencode.ToolPartStateStatusCompleted || part.State.Status != opencode.ToolPartStateStatusError {
// i don't think there's a case where a tool call isn't in result state // i don't think there's a case where a tool call isn't in result state
// and the message time is 0, but just in case // and the message time is 0, but just in case
finished = false finished = false
@ -195,17 +237,18 @@ func (m *messagesComponent) renderView(width int) {
} }
if finished { if finished {
key := m.cache.GenerateKey(message.ID, p.Text, width, m.showToolDetails, m.selectedPart == m.partCount) key := m.cache.GenerateKey(casted.ID, p.Text, width, m.showToolDetails, m.selectedPart == m.partCount)
content, cached = m.cache.Get(key) content, cached = m.cache.Get(key)
if !cached { if !cached {
content = renderText( content = renderText(
m.app, m.app,
message, message,
p.Text, p.Text,
message.Metadata.Assistant.ModelID, casted.ModelID,
m.showToolDetails, m.showToolDetails,
m.partCount == m.selectedPart, m.partCount == m.selectedPart,
width, width,
"",
toolCallParts..., toolCallParts...,
) )
m.cache.Set(key, content) m.cache.Set(key, content)
@ -215,10 +258,11 @@ func (m *messagesComponent) renderView(width int) {
m.app, m.app,
message, message,
p.Text, p.Text,
message.Metadata.Assistant.ModelID, casted.ModelID,
m.showToolDetails, m.showToolDetails,
m.partCount == m.selectedPart, m.partCount == m.selectedPart,
width, width,
"",
toolCallParts..., toolCallParts...,
) )
} }
@ -226,7 +270,7 @@ func (m *messagesComponent) renderView(width int) {
m = m.updateSelected(content, p.Text) m = m.updateSelected(content, p.Text)
blocks = append(blocks, content) blocks = append(blocks, content)
} }
case opencode.ToolInvocationPart: case opencode.ToolPart:
if !m.showToolDetails { if !m.showToolDetails {
if !hasTextPart { if !hasTextPart {
orphanedToolCalls = append(orphanedToolCalls, part) orphanedToolCalls = append(orphanedToolCalls, part)
@ -234,9 +278,9 @@ func (m *messagesComponent) renderView(width int) {
continue continue
} }
if part.ToolInvocation.State == "result" { if part.State.Status == opencode.ToolPartStateStatusCompleted || part.State.Status == opencode.ToolPartStateStatusError {
key := m.cache.GenerateKey(message.ID, key := m.cache.GenerateKey(casted.ID,
part.ToolInvocation.ToolCallID, part.ID,
m.showToolDetails, m.showToolDetails,
width, width,
m.partCount == m.selectedPart, m.partCount == m.selectedPart,
@ -246,7 +290,6 @@ func (m *messagesComponent) renderView(width int) {
content = renderToolDetails( content = renderToolDetails(
m.app, m.app,
part, part,
message.Metadata,
m.partCount == m.selectedPart, m.partCount == m.selectedPart,
width, width,
) )
@ -257,7 +300,6 @@ func (m *messagesComponent) renderView(width int) {
content = renderToolDetails( content = renderToolDetails(
m.app, m.app,
part, part,
message.Metadata,
m.partCount == m.selectedPart, m.partCount == m.selectedPart,
width, width,
) )
@ -271,15 +313,17 @@ func (m *messagesComponent) renderView(width int) {
} }
error := "" error := ""
switch err := message.Metadata.Error.AsUnion().(type) { if assistant, ok := message.(opencode.AssistantMessage); ok {
switch err := assistant.Error.AsUnion().(type) {
case nil: case nil:
case opencode.MessageMetadataErrorMessageOutputLengthError: case opencode.AssistantMessageErrorMessageOutputLengthError:
error = "Message output length exceeded" error = "Message output length exceeded"
case opencode.ProviderAuthError: case opencode.ProviderAuthError:
error = err.Data.Message error = err.Data.Message
case opencode.UnknownError: case opencode.UnknownError:
error = err.Data.Message error = err.Data.Message
} }
}
if error != "" { if error != "" {
error = renderContentBlock( error = renderContentBlock(

View file

@ -64,7 +64,7 @@ type CompletionProvider interface {
type CompletionSelectedMsg struct { type CompletionSelectedMsg struct {
SearchString string SearchString string
CompletionValue string CompletionValue string
IsCommand bool ProviderID string
} }
type CompletionDialogCompleteItemMsg struct { type CompletionDialogCompleteItemMsg struct {
@ -121,9 +121,6 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var query string var query string
query = c.pseudoSearchTextArea.Value() query = c.pseudoSearchTextArea.Value()
if query != "" {
query = query[1:]
}
if query != c.query { if query != c.query {
c.query = query c.query = query
@ -183,8 +180,9 @@ func (c *completionDialogComponent) View() string {
for _, cmd := range completions { for _, cmd := range completions {
title := cmd.DisplayValue() title := cmd.DisplayValue()
if len(title) > maxWidth-4 { width := lipgloss.Width(title)
maxWidth = len(title) + 4 if width > maxWidth-4 {
maxWidth = width + 4
} }
} }
@ -213,14 +211,11 @@ func (c *completionDialogComponent) IsEmpty() bool {
func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd { func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
value := c.pseudoSearchTextArea.Value() value := c.pseudoSearchTextArea.Value()
// Check if this is a command completion
isCommand := c.completionProvider.GetId() == "commands"
return tea.Batch( return tea.Batch(
util.CmdHandler(CompletionSelectedMsg{ util.CmdHandler(CompletionSelectedMsg{
SearchString: value, SearchString: value,
CompletionValue: item.GetValue(), CompletionValue: item.GetValue(),
IsCommand: isCommand, ProviderID: c.completionProvider.GetId(),
}), }),
c.close(), c.close(),
) )

View file

@ -124,7 +124,7 @@ func (f *findDialogComponent) View() string {
f.list.SetMaxWidth(f.width - 4) f.list.SetMaxWidth(f.width - 4)
inputView := f.textInput.View() inputView := f.textInput.View()
inputView = styles.NewStyle(). inputView = styles.NewStyle().
Background(t.BackgroundPanel()). Background(t.BackgroundElement()).
Height(1). Height(1).
Width(f.width-4). Width(f.width-4).
Padding(0, 0). Padding(0, 0).
@ -171,7 +171,7 @@ func (f *findDialogComponent) Close() tea.Cmd {
func createTextInput(existing *textinput.Model) textinput.Model { func createTextInput(existing *textinput.Model) textinput.Model {
t := theme.CurrentTheme() t := theme.CurrentTheme()
bgColor := t.BackgroundPanel() bgColor := t.BackgroundElement()
textColor := t.Text() textColor := t.Text()
textMutedColor := t.TextMuted() textMutedColor := t.TextMuted()

View file

@ -56,24 +56,24 @@ func (m ModelItem) Render(selected bool, width int) string {
displayText := fmt.Sprintf("%s (%s)", m.ModelName, m.ProviderName) displayText := fmt.Sprintf("%s (%s)", m.ModelName, m.ProviderName)
return styles.NewStyle(). return styles.NewStyle().
Background(t.Primary()). Background(t.Primary()).
Foreground(t.BackgroundElement()). Foreground(t.BackgroundPanel()).
Width(width). Width(width).
PaddingLeft(1). PaddingLeft(1).
Render(displayText) Render(displayText)
} else { } else {
modelStyle := styles.NewStyle(). modelStyle := styles.NewStyle().
Foreground(t.Text()). Foreground(t.Text()).
Background(t.BackgroundElement()) Background(t.BackgroundPanel())
providerStyle := styles.NewStyle(). providerStyle := styles.NewStyle().
Foreground(t.TextMuted()). Foreground(t.TextMuted()).
Background(t.BackgroundElement()) Background(t.BackgroundPanel())
modelPart := modelStyle.Render(m.ModelName) modelPart := modelStyle.Render(m.ModelName)
providerPart := providerStyle.Render(fmt.Sprintf(" (%s)", m.ProviderName)) providerPart := providerStyle.Render(fmt.Sprintf(" (%s)", m.ProviderName))
combinedText := modelPart + providerPart combinedText := modelPart + providerPart
return styles.NewStyle(). return styles.NewStyle().
Background(t.BackgroundElement()). Background(t.BackgroundPanel()).
PaddingLeft(1). PaddingLeft(1).
Render(combinedText) Render(combinedText)
} }

View file

@ -158,7 +158,12 @@ func (c *listComponent[T]) View() string {
return strings.Join(listItems, "\n") return strings.Join(listItems, "\n")
} }
func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[T] { func NewListComponent[T ListItem](
items []T,
maxVisibleItems int,
fallbackMsg string,
useAlphaNumericKeys bool,
) List[T] {
return &listComponent[T]{ return &listComponent[T]{
fallbackMsg: fallbackMsg, fallbackMsg: fallbackMsg,
items: items, items: items,
@ -194,7 +199,12 @@ func (s StringItem) Render(selected bool, width int) string {
} }
// NewStringList creates a new list component with string items // NewStringList creates a new list component with string items
func NewStringList(items []string, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[StringItem] { func NewStringList(
items []string,
maxVisibleItems int,
fallbackMsg string,
useAlphaNumericKeys bool,
) List[StringItem] {
stringItems := make([]StringItem, len(items)) stringItems := make([]StringItem, len(items))
for i, item := range items { for i, item := range items {
stringItems[i] = StringItem(item) stringItems[i] = StringItem(item)

View file

@ -90,7 +90,7 @@ func (m *Modal) Render(contentView string, background string) string {
innerWidth := outerWidth - 4 innerWidth := outerWidth - 4
baseStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement()) baseStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel())
var finalContent string var finalContent string
if m.title != "" { if m.title != "" {
@ -140,6 +140,6 @@ func (m *Modal) Render(contentView string, background string) string {
modalView, modalView,
background, background,
layout.WithOverlayBorder(), layout.WithOverlayBorder(),
layout.WithOverlayBorderColor(t.BorderActive()), layout.WithOverlayBorderColor(t.Primary()),
) )
} }

View file

@ -23,7 +23,7 @@ func Generate(text string) (string, int, error) {
} }
// Create lipgloss style for QR code with theme colors // Create lipgloss style for QR code with theme colors
qrStyle := styles.NewStyleWithColors(t.Text(), t.Background()) qrStyle := styles.NewStyle().Foreground(t.Text()).Background(t.Background())
var result strings.Builder var result strings.Builder

View file

@ -6,6 +6,7 @@ import (
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
@ -101,10 +102,11 @@ func (m statusComponent) View() string {
contextWindow := m.app.Model.Limit.Context contextWindow := m.app.Model.Limit.Context
for _, message := range m.app.Messages { for _, message := range m.app.Messages {
cost += message.Metadata.Assistant.Cost if assistant, ok := message.(opencode.AssistantMessage); ok {
usage := message.Metadata.Assistant.Tokens cost += assistant.Cost
usage := assistant.Tokens
if usage.Output > 0 { if usage.Output > 0 {
if message.Metadata.Assistant.Summary { if assistant.Summary {
tokens = usage.Output tokens = usage.Output
continue continue
} }
@ -115,6 +117,7 @@ func (m statusComponent) View() string {
usage.Reasoning) usage.Reasoning)
} }
} }
}
sessionInfo = styles.NewStyle(). sessionInfo = styles.NewStyle().
Foreground(t.TextMuted()). Foreground(t.TextMuted()).

File diff suppressed because it is too large Load diff

View file

@ -1,41 +0,0 @@
package layout_test
import (
"fmt"
"github.com/sst/opencode/internal/layout"
)
func ExampleRender_withGap() {
// Create a horizontal layout with 3px gap between items
result := layout.Render(
layout.FlexOptions{
Direction: layout.Row,
Width: 30,
Height: 1,
Gap: 3,
},
layout.FlexItem{View: "Item1"},
layout.FlexItem{View: "Item2"},
layout.FlexItem{View: "Item3"},
)
fmt.Println(result)
// Output: Item1 Item2 Item3
}
func ExampleRender_withGapAndJustify() {
// Create a horizontal layout with gap and space-between justification
result := layout.Render(
layout.FlexOptions{
Direction: layout.Row,
Width: 30,
Height: 1,
Gap: 2,
Justify: layout.JustifySpaceBetween,
},
layout.FlexItem{View: "A"},
layout.FlexItem{View: "B"},
layout.FlexItem{View: "C"},
)
fmt.Println(result)
// Output: A B C
}

View file

@ -1,90 +0,0 @@
package layout
import (
"strings"
"testing"
)
func TestFlexGap(t *testing.T) {
tests := []struct {
name string
opts FlexOptions
items []FlexItem
expected string
}{
{
name: "Row with gap",
opts: FlexOptions{
Direction: Row,
Width: 20,
Height: 1,
Gap: 2,
},
items: []FlexItem{
{View: "A"},
{View: "B"},
{View: "C"},
},
expected: "A B C",
},
{
name: "Column with gap",
opts: FlexOptions{
Direction: Column,
Width: 1,
Height: 5,
Gap: 1,
Align: AlignStart,
},
items: []FlexItem{
{View: "A", FixedSize: 1},
{View: "B", FixedSize: 1},
{View: "C", FixedSize: 1},
},
expected: "A\n \nB\n \nC",
},
{
name: "Row with gap and justify space between",
opts: FlexOptions{
Direction: Row,
Width: 15,
Height: 1,
Gap: 1,
Justify: JustifySpaceBetween,
},
items: []FlexItem{
{View: "A"},
{View: "B"},
{View: "C"},
},
expected: "A B C",
},
{
name: "No gap specified",
opts: FlexOptions{
Direction: Row,
Width: 10,
Height: 1,
},
items: []FlexItem{
{View: "A"},
{View: "B"},
{View: "C"},
},
expected: "ABC",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Render(tt.opts, tt.items...)
// Trim any trailing spaces for comparison
result = strings.TrimRight(result, " ")
expected := strings.TrimRight(tt.expected, " ")
if result != expected {
t.Errorf("Render() = %q, want %q", result, expected)
}
})
}
}

View file

@ -52,7 +52,9 @@ type appModel struct {
messages chat.MessagesComponent messages chat.MessagesComponent
completions dialog.CompletionDialog completions dialog.CompletionDialog
commandProvider dialog.CompletionProvider commandProvider dialog.CompletionProvider
fileProvider dialog.CompletionProvider
showCompletionDialog bool showCompletionDialog bool
fileCompletionActive bool
leaderBinding *key.Binding leaderBinding *key.Binding
isLeaderSequence bool isLeaderSequence bool
toastManager *toast.ToastManager toastManager *toast.ToastManager
@ -180,11 +182,33 @@ 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)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
// Set command provider for command completion
a.completions = dialog.NewCompletionDialogComponent(a.commandProvider)
updated, cmd = a.completions.Update(msg)
a.completions = updated.(dialog.CompletionDialog)
cmds = append(cmds, cmd)
return a, tea.Sequence(cmds...)
}
// Handle file completions trigger
if keyString == "@" &&
!a.showCompletionDialog {
a.showCompletionDialog = true
a.fileCompletionActive = true
updated, cmd := a.editor.Update(msg)
a.editor = updated.(chat.EditorComponent)
cmds = append(cmds, cmd)
// Set file provider for file completion
a.completions = dialog.NewCompletionDialogComponent(a.fileProvider)
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)
@ -194,7 +218,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": case "tab", "enter", "esc", "ctrl+c", "up", "down":
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)
@ -326,10 +350,11 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, toast.NewErrorToast(msg.Error()) return a, toast.NewErrorToast(msg.Error())
case app.SendMsg: case app.SendMsg:
a.showCompletionDialog = false a.showCompletionDialog = false
cmd := a.app.SendChatMessage(context.Background(), msg.Text, msg.Attachments) a.app, cmd = a.app.SendChatMessage(context.Background(), msg.Text, msg.Attachments)
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.",
@ -338,7 +363,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case opencode.EventListResponseEventSessionDeleted: case opencode.EventListResponseEventSessionDeleted:
if a.app.Session != nil && msg.Properties.Info.ID == a.app.Session.ID { if a.app.Session != nil && msg.Properties.Info.ID == a.app.Session.ID {
a.app.Session = &opencode.Session{} a.app.Session = &opencode.Session{}
a.app.Messages = []opencode.Message{} a.app.Messages = []opencode.MessageUnion{}
} }
return a, toast.NewSuccessToast("Session deleted successfully") return a, toast.NewSuccessToast("Session deleted successfully")
case opencode.EventListResponseEventSessionUpdated: case opencode.EventListResponseEventSessionUpdated:
@ -346,7 +371,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.app.Session = &msg.Properties.Info a.app.Session = &msg.Properties.Info
} }
case opencode.EventListResponseEventMessageUpdated: case opencode.EventListResponseEventMessageUpdated:
if msg.Properties.Info.Metadata.SessionID == a.app.Session.ID { if msg.Properties.Info.SessionID == a.app.Session.ID {
exists := false exists := false
optimisticReplaced := false optimisticReplaced := false
@ -354,21 +379,31 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg.Properties.Info.Role == opencode.MessageRoleUser { if msg.Properties.Info.Role == opencode.MessageRoleUser {
// Look for optimistic messages to replace // Look for optimistic messages to replace
for i, m := range a.app.Messages { for i, m := range a.app.Messages {
if strings.HasPrefix(m.ID, "optimistic-") && m.Role == opencode.MessageRoleUser { switch m := m.(type) {
case opencode.UserMessage:
if strings.HasPrefix(m.ID, "optimistic-") && m.Role == opencode.UserMessageRoleUser {
// Replace the optimistic message with the real one // Replace the optimistic message with the real one
a.app.Messages[i] = msg.Properties.Info a.app.Messages[i] = msg.Properties.Info.AsUnion()
exists = true exists = true
optimisticReplaced = true optimisticReplaced = true
break break
} }
} }
} }
}
// If not replacing optimistic, check for existing message with same ID // If not replacing optimistic, check for existing message with same ID
if !optimisticReplaced { if !optimisticReplaced {
for i, m := range a.app.Messages { for i, m := range a.app.Messages {
if m.ID == msg.Properties.Info.ID { var id string
a.app.Messages[i] = msg.Properties.Info switch m := m.(type) {
case opencode.UserMessage:
id = m.ID
case opencode.AssistantMessage:
id = m.ID
}
if id == msg.Properties.Info.ID {
a.app.Messages[i] = msg.Properties.Info.AsUnion()
exists = true exists = true
break break
} }
@ -376,7 +411,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
if !exists { if !exists {
a.app.Messages = append(a.app.Messages, msg.Properties.Info) a.app.Messages = append(a.app.Messages, msg.Properties.Info.AsUnion())
} }
} }
case opencode.EventListResponseEventSessionError: case opencode.EventListResponseEventSessionError:
@ -437,7 +472,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
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 = make([]opencode.MessageUnion, 0)
for _, message := range messages {
a.app.Messages = append(a.app.Messages, message.AsUnion())
}
return a, util.CmdHandler(app.SessionLoadedMsg{}) return a, util.CmdHandler(app.SessionLoadedMsg{})
case app.ModelSelectedMsg: case app.ModelSelectedMsg:
a.app.Provider = &msg.Provider a.app.Provider = &msg.Provider
@ -778,11 +816,8 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
return nil return nil
} }
os.Remove(tmpfile.Name()) os.Remove(tmpfile.Name())
// attachments := m.attachments
// m.attachments = nil
return app.SendMsg{ return app.SendMsg{
Text: string(content), Text: string(content),
Attachments: []app.Attachment{}, // attachments,
} }
}) })
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
@ -791,7 +826,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
return a, nil return a, nil
} }
a.app.Session = &opencode.Session{} a.app.Session = &opencode.Session{}
a.app.Messages = []opencode.Message{} a.app.Messages = []opencode.MessageUnion{}
cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{})) cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{}))
case commands.SessionListCommand: case commands.SessionListCommand:
sessionDialog := dialog.NewSessionDialog(a.app) sessionDialog := dialog.NewSessionDialog(a.app)
@ -954,6 +989,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
func NewModel(app *app.App) tea.Model { func NewModel(app *app.App) tea.Model {
commandProvider := completions.NewCommandCompletionProvider(app) commandProvider := completions.NewCommandCompletionProvider(app)
fileProvider := completions.NewFileAndFolderContextGroup(app)
messages := chat.NewMessagesComponent(app) messages := chat.NewMessagesComponent(app)
editor := chat.NewEditorComponent(app) editor := chat.NewEditorComponent(app)
@ -972,9 +1008,11 @@ func NewModel(app *app.App) tea.Model {
messages: messages, messages: messages,
completions: completions, completions: completions,
commandProvider: commandProvider, commandProvider: commandProvider,
fileProvider: fileProvider,
leaderBinding: leaderBinding, leaderBinding: leaderBinding,
isLeaderSequence: false, isLeaderSequence: false,
showCompletionDialog: false, showCompletionDialog: false,
fileCompletionActive: false,
toastManager: toast.NewToastManager(), toastManager: toast.NewToastManager(),
interruptKeyState: InterruptKeyIdle, interruptKeyState: InterruptKeyIdle,
fileViewer: fileviewer.New(app), fileViewer: fileviewer.New(app),

View file

@ -83,7 +83,7 @@ func Extension(path string) string {
} }
func ToMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string { func ToMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string {
r := styles.GetMarkdownRenderer(width-7, backgroundColor) r := styles.GetMarkdownRenderer(width-6, backgroundColor)
content = strings.ReplaceAll(content, RootPath+"/", "") content = strings.ReplaceAll(content, RootPath+"/", "")
rendered, _ := r.Render(content) rendered, _ := r.Render(content)
lines := strings.Split(rendered, "\n") lines := strings.Split(rendered, "\n")

View file

@ -2,15 +2,15 @@ name: CI
on: on:
push: push:
branches-ignore: branches-ignore:
- 'generated' - "generated"
- 'codegen/**' - "codegen/**"
- 'integrated/**' - "integrated/**"
- 'stl-preview-head/**' - "stl-preview-head/**"
- 'stl-preview-base/**' - "stl-preview-base/**"
pull_request: pull_request:
branches-ignore: branches-ignore:
- 'stl-preview-head/**' - "stl-preview-head/**"
- 'stl-preview-base/**' - "stl-preview-base/**"
jobs: jobs:
lint: lint:

View file

@ -1,4 +1,4 @@
configured_endpoints: 20 configured_endpoints: 20
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-945f9da9e9a4c4008834deef63e4346c0076e020eed3d3c98c249095033c1ac5.yml openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-15eeb028f79b9a065b4e54a6ea6a58631e9bd5004f97820f0c79d18e3f8bac84.yml
openapi_spec_hash: 522a44f6cb0677435fe2ac7693848ad7 openapi_spec_hash: 38c8bacb6c8e4c46852a3e81e3fb9fda
config_hash: 6c8822d278ba83456e5eed6d774ca230 config_hash: e03e9d1aad76081fa1163086e89f201b

View file

@ -6,7 +6,7 @@ Full Changelog: [v0.1.0-alpha.7...v0.1.0-alpha.8](https://github.com/sst/opencod
### Features ### Features
* **api:** update via SDK Studio ([651e937](https://github.com/sst/opencode-sdk-go/commit/651e937c334e1caba3b968e6cac865c219879519)) - **api:** update via SDK Studio ([651e937](https://github.com/sst/opencode-sdk-go/commit/651e937c334e1caba3b968e6cac865c219879519))
## 0.1.0-alpha.7 (2025-06-30) ## 0.1.0-alpha.7 (2025-06-30)
@ -14,13 +14,12 @@ Full Changelog: [v0.1.0-alpha.6...v0.1.0-alpha.7](https://github.com/sst/opencod
### Features ### Features
* **api:** update via SDK Studio ([13550a5](https://github.com/sst/opencode-sdk-go/commit/13550a5c65d77325e945ed99fe0799cd1107b775)) - **api:** update via SDK Studio ([13550a5](https://github.com/sst/opencode-sdk-go/commit/13550a5c65d77325e945ed99fe0799cd1107b775))
* **api:** update via SDK Studio ([7b73730](https://github.com/sst/opencode-sdk-go/commit/7b73730c7fa62ba966dda3541c3e97b49be8d2bf)) - **api:** update via SDK Studio ([7b73730](https://github.com/sst/opencode-sdk-go/commit/7b73730c7fa62ba966dda3541c3e97b49be8d2bf))
### Chores ### Chores
* **ci:** only run for pushes and fork pull requests ([bea59b8](https://github.com/sst/opencode-sdk-go/commit/bea59b886800ef555f89c47a9256d6392ed2e53d)) - **ci:** only run for pushes and fork pull requests ([bea59b8](https://github.com/sst/opencode-sdk-go/commit/bea59b886800ef555f89c47a9256d6392ed2e53d))
## 0.1.0-alpha.6 (2025-06-28) ## 0.1.0-alpha.6 (2025-06-28)
@ -28,7 +27,7 @@ Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/sst/opencod
### Bug Fixes ### Bug Fixes
* don't try to deserialize as json when ResponseBodyInto is []byte ([5988d04](https://github.com/sst/opencode-sdk-go/commit/5988d04839cb78b6613057280b91b72a60fef33d)) - don't try to deserialize as json when ResponseBodyInto is []byte ([5988d04](https://github.com/sst/opencode-sdk-go/commit/5988d04839cb78b6613057280b91b72a60fef33d))
## 0.1.0-alpha.5 (2025-06-27) ## 0.1.0-alpha.5 (2025-06-27)
@ -36,7 +35,7 @@ Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/sst/opencod
### Features ### Features
* **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e)) - **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e))
## 0.1.0-alpha.4 (2025-06-27) ## 0.1.0-alpha.4 (2025-06-27)
@ -44,7 +43,7 @@ Full Changelog: [v0.1.0-alpha.3...v0.1.0-alpha.4](https://github.com/sst/opencod
### Features ### Features
* **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52)) - **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52))
## 0.1.0-alpha.3 (2025-06-27) ## 0.1.0-alpha.3 (2025-06-27)
@ -52,7 +51,7 @@ Full Changelog: [v0.1.0-alpha.2...v0.1.0-alpha.3](https://github.com/sst/opencod
### Features ### Features
* **api:** update via SDK Studio ([57f3230](https://github.com/sst/opencode-sdk-go/commit/57f32309023cc1f0f20c20d02a3907e390a71f61)) - **api:** update via SDK Studio ([57f3230](https://github.com/sst/opencode-sdk-go/commit/57f32309023cc1f0f20c20d02a3907e390a71f61))
## 0.1.0-alpha.2 (2025-06-27) ## 0.1.0-alpha.2 (2025-06-27)
@ -60,7 +59,7 @@ Full Changelog: [v0.1.0-alpha.1...v0.1.0-alpha.2](https://github.com/sst/opencod
### Features ### Features
* **api:** update via SDK Studio ([a766f1c](https://github.com/sst/opencode-sdk-go/commit/a766f1c54f02bbc1380151b0e22d97cc2c5892e6)) - **api:** update via SDK Studio ([a766f1c](https://github.com/sst/opencode-sdk-go/commit/a766f1c54f02bbc1380151b0e22d97cc2c5892e6))
## 0.1.0-alpha.1 (2025-06-27) ## 0.1.0-alpha.1 (2025-06-27)
@ -68,6 +67,6 @@ Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/sst/opencod
### Features ### Features
* **api:** update via SDK Studio ([27b7376](https://github.com/sst/opencode-sdk-go/commit/27b7376310466ee17a63f2104f546b53a2b8361a)) - **api:** update via SDK Studio ([27b7376](https://github.com/sst/opencode-sdk-go/commit/27b7376310466ee17a63f2104f546b53a2b8361a))
* **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397)) - **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397))
* **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844)) - **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844))

View file

@ -71,30 +71,25 @@ Methods:
Params Types: Params 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#FilePartParam">FilePartParam</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#FilePartParam">FilePartParam</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#MessagePartUnionParam">MessagePartUnionParam</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#ReasoningPartParam">ReasoningPartParam</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#SourceURLPartParam">SourceURLPartParam</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#StepStartPartParam">StepStartPartParam</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#TextPartParam">TextPartParam</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#TextPartParam">TextPartParam</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#ToolCallParam">ToolCallParam</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#UserMessagePartUnionParam">UserMessagePartUnionParam</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#ToolInvocationPartParam">ToolInvocationPartParam</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#ToolPartialCallParam">ToolPartialCallParam</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#ToolResultParam">ToolResultParam</a>
Response Types: 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#AssistantMessage">AssistantMessage</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#AssistantMessagePart">AssistantMessagePart</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#FilePart">FilePart</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#FilePart">FilePart</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#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#MessagePart">MessagePart</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#ReasoningPart">ReasoningPart</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#SourceURLPart">SourceURLPart</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>
- <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#ToolCall">ToolCall</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#ToolPart">ToolPart</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#ToolInvocationPart">ToolInvocationPart</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#ToolStateCompleted">ToolStateCompleted</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#ToolPartialCall">ToolPartialCall</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#ToolStateError">ToolStateError</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#ToolResult">ToolResult</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#ToolStatePending">ToolStatePending</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#ToolStateRunning">ToolStateRunning</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#UserMessage">UserMessage</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#UserMessagePart">UserMessagePart</a>
Methods: Methods:
@ -102,7 +97,7 @@ Methods:
- <code title="get /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</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/builtin#error">error</a>)</code> - <code title="get /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</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/builtin#error">error</a>)</code>
- <code title="delete /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> - <code title="delete /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> - <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <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#SessionChatParams">SessionChatParams</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/builtin#error">error</a>)</code> - <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <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#SessionChatParams">SessionChatParams</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#AssistantMessage">AssistantMessage</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <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#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code> - <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <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#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</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/builtin#error">error</a>)</code> - <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</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/builtin#error">error</a>)</code>
- <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</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/builtin#error">error</a>)</code> - <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</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/builtin#error">error</a>)</code>

View file

@ -51,8 +51,8 @@ type EventListResponse struct {
// [EventListResponseEventLspClientDiagnosticsProperties], // [EventListResponseEventLspClientDiagnosticsProperties],
// [EventListResponseEventPermissionUpdatedProperties], // [EventListResponseEventPermissionUpdatedProperties],
// [EventListResponseEventFileEditedProperties], // [EventListResponseEventFileEditedProperties],
// [EventListResponseEventStorageWriteProperties],
// [EventListResponseEventInstallationUpdatedProperties], // [EventListResponseEventInstallationUpdatedProperties],
// [EventListResponseEventStorageWriteProperties],
// [EventListResponseEventMessageUpdatedProperties], // [EventListResponseEventMessageUpdatedProperties],
// [EventListResponseEventMessageRemovedProperties], // [EventListResponseEventMessageRemovedProperties],
// [EventListResponseEventMessagePartUpdatedProperties], // [EventListResponseEventMessagePartUpdatedProperties],
@ -95,9 +95,9 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) {
// Possible runtime types of the union are // Possible runtime types of the union are
// [EventListResponseEventLspClientDiagnostics], // [EventListResponseEventLspClientDiagnostics],
// [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited], // [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited],
// [EventListResponseEventStorageWrite],
// [EventListResponseEventInstallationUpdated], // [EventListResponseEventInstallationUpdated],
// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved], // [EventListResponseEventStorageWrite], [EventListResponseEventMessageUpdated],
// [EventListResponseEventMessageRemoved],
// [EventListResponseEventMessagePartUpdated], // [EventListResponseEventMessagePartUpdated],
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError], // [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
@ -108,9 +108,9 @@ func (r EventListResponse) AsUnion() EventListResponseUnion {
// Union satisfied by [EventListResponseEventLspClientDiagnostics], // Union satisfied by [EventListResponseEventLspClientDiagnostics],
// [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited], // [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited],
// [EventListResponseEventStorageWrite],
// [EventListResponseEventInstallationUpdated], // [EventListResponseEventInstallationUpdated],
// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved], // [EventListResponseEventStorageWrite], [EventListResponseEventMessageUpdated],
// [EventListResponseEventMessageRemoved],
// [EventListResponseEventMessagePartUpdated], // [EventListResponseEventMessagePartUpdated],
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or // [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or
@ -140,13 +140,13 @@ func init() {
}, },
apijson.UnionVariant{ apijson.UnionVariant{
TypeFilter: gjson.JSON, TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventStorageWrite{}), Type: reflect.TypeOf(EventListResponseEventInstallationUpdated{}),
DiscriminatorValue: "storage.write", DiscriminatorValue: "installation.updated",
}, },
apijson.UnionVariant{ apijson.UnionVariant{
TypeFilter: gjson.JSON, TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventInstallationUpdated{}), Type: reflect.TypeOf(EventListResponseEventStorageWrite{}),
DiscriminatorValue: "installation.updated", DiscriminatorValue: "storage.write",
}, },
apijson.UnionVariant{ apijson.UnionVariant{
TypeFilter: gjson.JSON, TypeFilter: gjson.JSON,
@ -402,6 +402,66 @@ func (r EventListResponseEventFileEditedType) IsKnown() bool {
return false return false
} }
type EventListResponseEventInstallationUpdated struct {
Properties EventListResponseEventInstallationUpdatedProperties `json:"properties,required"`
Type EventListResponseEventInstallationUpdatedType `json:"type,required"`
JSON eventListResponseEventInstallationUpdatedJSON `json:"-"`
}
// eventListResponseEventInstallationUpdatedJSON contains the JSON metadata for the
// struct [EventListResponseEventInstallationUpdated]
type eventListResponseEventInstallationUpdatedJSON struct {
Properties apijson.Field
Type apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *EventListResponseEventInstallationUpdated) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r eventListResponseEventInstallationUpdatedJSON) RawJSON() string {
return r.raw
}
func (r EventListResponseEventInstallationUpdated) implementsEventListResponse() {}
type EventListResponseEventInstallationUpdatedProperties struct {
Version string `json:"version,required"`
JSON eventListResponseEventInstallationUpdatedPropertiesJSON `json:"-"`
}
// eventListResponseEventInstallationUpdatedPropertiesJSON contains the JSON
// metadata for the struct [EventListResponseEventInstallationUpdatedProperties]
type eventListResponseEventInstallationUpdatedPropertiesJSON struct {
Version apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *EventListResponseEventInstallationUpdatedProperties) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r eventListResponseEventInstallationUpdatedPropertiesJSON) RawJSON() string {
return r.raw
}
type EventListResponseEventInstallationUpdatedType string
const (
EventListResponseEventInstallationUpdatedTypeInstallationUpdated EventListResponseEventInstallationUpdatedType = "installation.updated"
)
func (r EventListResponseEventInstallationUpdatedType) IsKnown() bool {
switch r {
case EventListResponseEventInstallationUpdatedTypeInstallationUpdated:
return true
}
return false
}
type EventListResponseEventStorageWrite struct { type EventListResponseEventStorageWrite struct {
Properties EventListResponseEventStorageWriteProperties `json:"properties,required"` Properties EventListResponseEventStorageWriteProperties `json:"properties,required"`
Type EventListResponseEventStorageWriteType `json:"type,required"` Type EventListResponseEventStorageWriteType `json:"type,required"`
@ -464,66 +524,6 @@ func (r EventListResponseEventStorageWriteType) IsKnown() bool {
return false return false
} }
type EventListResponseEventInstallationUpdated struct {
Properties EventListResponseEventInstallationUpdatedProperties `json:"properties,required"`
Type EventListResponseEventInstallationUpdatedType `json:"type,required"`
JSON eventListResponseEventInstallationUpdatedJSON `json:"-"`
}
// eventListResponseEventInstallationUpdatedJSON contains the JSON metadata for the
// struct [EventListResponseEventInstallationUpdated]
type eventListResponseEventInstallationUpdatedJSON struct {
Properties apijson.Field
Type apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *EventListResponseEventInstallationUpdated) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r eventListResponseEventInstallationUpdatedJSON) RawJSON() string {
return r.raw
}
func (r EventListResponseEventInstallationUpdated) implementsEventListResponse() {}
type EventListResponseEventInstallationUpdatedProperties struct {
Version string `json:"version,required"`
JSON eventListResponseEventInstallationUpdatedPropertiesJSON `json:"-"`
}
// eventListResponseEventInstallationUpdatedPropertiesJSON contains the JSON
// metadata for the struct [EventListResponseEventInstallationUpdatedProperties]
type eventListResponseEventInstallationUpdatedPropertiesJSON struct {
Version apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *EventListResponseEventInstallationUpdatedProperties) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r eventListResponseEventInstallationUpdatedPropertiesJSON) RawJSON() string {
return r.raw
}
type EventListResponseEventInstallationUpdatedType string
const (
EventListResponseEventInstallationUpdatedTypeInstallationUpdated EventListResponseEventInstallationUpdatedType = "installation.updated"
)
func (r EventListResponseEventInstallationUpdatedType) IsKnown() bool {
switch r {
case EventListResponseEventInstallationUpdatedTypeInstallationUpdated:
return true
}
return false
}
type EventListResponseEventMessageUpdated struct { type EventListResponseEventMessageUpdated struct {
Properties EventListResponseEventMessageUpdatedProperties `json:"properties,required"` Properties EventListResponseEventMessageUpdatedProperties `json:"properties,required"`
Type EventListResponseEventMessageUpdatedType `json:"type,required"` Type EventListResponseEventMessageUpdatedType `json:"type,required"`
@ -673,7 +673,7 @@ func (r EventListResponseEventMessagePartUpdated) implementsEventListResponse()
type EventListResponseEventMessagePartUpdatedProperties struct { type EventListResponseEventMessagePartUpdatedProperties struct {
MessageID string `json:"messageID,required"` MessageID string `json:"messageID,required"`
Part MessagePart `json:"part,required"` Part AssistantMessagePart `json:"part,required"`
SessionID string `json:"sessionID,required"` SessionID string `json:"sessionID,required"`
JSON eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"` JSON eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"`
} }
@ -1159,8 +1159,8 @@ const (
EventListResponseTypeLspClientDiagnostics EventListResponseType = "lsp.client.diagnostics" EventListResponseTypeLspClientDiagnostics EventListResponseType = "lsp.client.diagnostics"
EventListResponseTypePermissionUpdated EventListResponseType = "permission.updated" EventListResponseTypePermissionUpdated EventListResponseType = "permission.updated"
EventListResponseTypeFileEdited EventListResponseType = "file.edited" EventListResponseTypeFileEdited EventListResponseType = "file.edited"
EventListResponseTypeStorageWrite EventListResponseType = "storage.write"
EventListResponseTypeInstallationUpdated EventListResponseType = "installation.updated" EventListResponseTypeInstallationUpdated EventListResponseType = "installation.updated"
EventListResponseTypeStorageWrite EventListResponseType = "storage.write"
EventListResponseTypeMessageUpdated EventListResponseType = "message.updated" EventListResponseTypeMessageUpdated EventListResponseType = "message.updated"
EventListResponseTypeMessageRemoved EventListResponseType = "message.removed" EventListResponseTypeMessageRemoved EventListResponseType = "message.removed"
EventListResponseTypeMessagePartUpdated EventListResponseType = "message.part.updated" EventListResponseTypeMessagePartUpdated EventListResponseType = "message.part.updated"
@ -1173,7 +1173,7 @@ const (
func (r EventListResponseType) IsKnown() bool { func (r EventListResponseType) IsKnown() bool {
switch r { switch r {
case EventListResponseTypeLspClientDiagnostics, EventListResponseTypePermissionUpdated, EventListResponseTypeFileEdited, EventListResponseTypeStorageWrite, EventListResponseTypeInstallationUpdated, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeFileWatcherUpdated: case EventListResponseTypeLspClientDiagnostics, EventListResponseTypePermissionUpdated, EventListResponseTypeFileEdited, EventListResponseTypeInstallationUpdated, EventListResponseTypeStorageWrite, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeFileWatcherUpdated:
return true return true
} }
return false return false

View file

@ -60,8 +60,5 @@
} }
], ],
"release-type": "go", "release-type": "go",
"extra-files": [ "extra-files": ["internal/version.go", "README.md"]
"internal/version.go",
"README.md"
]
} }

File diff suppressed because it is too large Load diff

View file

@ -118,7 +118,7 @@ func TestSessionChat(t *testing.T) {
"id", "id",
opencode.SessionChatParams{ opencode.SessionChatParams{
ModelID: opencode.F("modelID"), ModelID: opencode.F("modelID"),
Parts: opencode.F([]opencode.MessagePartUnionParam{opencode.TextPartParam{ Parts: opencode.F([]opencode.UserMessagePartUnionParam{opencode.TextPartParam{
Text: opencode.F("text"), Text: opencode.F("text"),
Type: opencode.F(opencode.TextPartTypeText), Type: opencode.F(opencode.TextPartTypeText),
}}), }}),

View file

@ -31,7 +31,7 @@ func (r providerAuthErrorJSON) RawJSON() string {
func (r ProviderAuthError) ImplementsEventListResponseEventSessionErrorPropertiesError() {} func (r ProviderAuthError) ImplementsEventListResponseEventSessionErrorPropertiesError() {}
func (r ProviderAuthError) ImplementsMessageMetadataError() {} func (r ProviderAuthError) ImplementsAssistantMessageError() {}
type ProviderAuthErrorData struct { type ProviderAuthErrorData struct {
Message string `json:"message,required"` Message string `json:"message,required"`
@ -94,7 +94,7 @@ func (r unknownErrorJSON) RawJSON() string {
func (r UnknownError) ImplementsEventListResponseEventSessionErrorPropertiesError() {} func (r UnknownError) ImplementsEventListResponseEventSessionErrorPropertiesError() {}
func (r UnknownError) ImplementsMessageMetadataError() {} func (r UnknownError) ImplementsAssistantMessageError() {}
type UnknownErrorData struct { type UnknownErrorData struct {
Message string `json:"message,required"` Message string `json:"message,required"`

View file

@ -20,18 +20,21 @@ export default defineConfig({
devToolbar: { devToolbar: {
enabled: false, enabled: false,
}, },
server: {
host: "0.0.0.0",
},
markdown: { markdown: {
rehypePlugins: [ rehypePlugins: [rehypeHeadingIds, [rehypeAutolinkHeadings, { behavior: "wrap" }]],
rehypeHeadingIds,
[rehypeAutolinkHeadings, { behavior: "wrap" }],
],
}, },
integrations: [ integrations: [
solidJs(), solidJs(),
starlight({ starlight({
title: "opencode", title: "opencode",
expressiveCode: { themes: ["github-light", "github-dark"] }, expressiveCode: { themes: ["github-light", "github-dark"] },
social: [{ icon: "github", label: "GitHub", href: config.github }], social: [
{ icon: "github", label: "GitHub", href: config.github },
{ icon: "discord", label: "Dscord", href: config.discord },
],
head: [ head: [
{ {
tag: "link", tag: "link",

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