Compare commits

..

95 Commits

Author SHA1 Message Date
Kit Langton db75770226 test(permission): migrate next tests to effect runner 2026-05-12 22:16:11 -04:00
Kit Langton 911b2ac088 test(session): migrate prompt tests to effect runner (#27209) 2026-05-13 01:51:35 +00:00
Kit Langton da689d77c4 effect: move tool flags into RuntimeFlags (#27198) 2026-05-12 21:45:53 -04:00
opencode-agent[bot] c9df833d2c chore: generate 2026-05-13 01:45:20 +00:00
Aiden Cline c2b1ebd9dc feat: update pricing schema for models to ensure more accurate cost tracking (#27184) 2026-05-12 20:44:06 -05:00
Kit Langton d1356f509e test(server): migrate HTTP API SDK test to Effect runner (#27208) 2026-05-13 01:43:35 +00:00
opencode-agent[bot] 69feb224ba chore: update nix node_modules hashes 2026-05-13 01:43:08 +00:00
Kit Langton 7d9ac713a9 test(config): migrate TUI config tests to Effect runner (#27206) 2026-05-13 01:42:33 +00:00
Kit Langton 4d9c675b00 test(server): migrate httpapi session test to effect runner (#27207) 2026-05-13 01:40:25 +00:00
Kit Langton 604de70689 test(server): migrate experimental HttpApi test to Effect (#27204) 2026-05-13 01:40:07 +00:00
opencode-agent[bot] 673226d214 chore: generate 2026-05-13 01:39:46 +00:00
Kit Langton 2447f42294 test(server): migrate session select to effect runner (#27203) 2026-05-13 01:38:38 +00:00
Frank 005e64e7d8 zen: stat worker 2026-05-12 21:37:41 -04:00
Kit Langton 8310e7df70 test(server): migrate missing patch diff test (#27202) 2026-05-13 01:37:34 +00:00
Kit Langton c4e676b8a0 fix(task): preserve subagent self permissions (#27201) 2026-05-13 01:36:02 +00:00
opencode-agent[bot] c2c40b5b61 chore: generate 2026-05-13 01:28:35 +00:00
Kit Langton 456495289e fix(httpapi): drop redundant InstanceRef/WorkspaceRef in session prompt handler (#27196) 2026-05-13 01:27:08 +00:00
Sebastian d6367853ae Add TUI notifications and attention sounds (disabled by default) (#26980) 2026-05-13 03:26:25 +02:00
opencode-agent[bot] adccab5970 chore: generate 2026-05-13 01:26:12 +00:00
Kit Langton b6d3fa09bc effect(core): add AppProcess service (Phase 1) (#27178) 2026-05-13 01:24:53 +00:00
Kit Langton 31e2d72bf7 test(worktree): scope created worktree cleanup (#27191) 2026-05-12 21:09:58 -04:00
Kit Langton b7c6fa611f effect: add RuntimeFlags service (#27181) 2026-05-12 20:59:02 -04:00
opencode-agent[bot] d0b8ff0f22 chore: update nix node_modules hashes 2026-05-13 00:57:32 +00:00
Kit Langton 59b976b954 Remove TUI logo sound effects (#27183) 2026-05-12 20:37:16 -04:00
Luke Parker d1640c075b fix(app): use session status for busy state (#27166) 2026-05-13 10:25:28 +10:00
Kit Langton 17d4c366e6 test(worktree): dispose created instances before removal (#27182) 2026-05-12 20:23:20 -04:00
Kit Langton d0844c600b test(worktree): use timeoutOrElse for ready wait (#27180) 2026-05-12 20:14:05 -04:00
Dax Raad 1d243ce25a harden cache usage 2026-05-12 20:06:46 -04:00
Kit Langton d8c8322ddf test: migrate worktree tests to effect runner (#27177) 2026-05-12 19:55:07 -04:00
Kit Langton 1b6599f411 test(plugin): use noop dependency boundaries (#27148) 2026-05-12 19:53:46 -04:00
opencode-agent[bot] 937a3c10b3 chore: generate 2026-05-12 23:49:28 +00:00
Kit Langton 72ce24c200 test: migrate effect cmd ALS test (#27173) 2026-05-12 23:48:17 +00:00
Kit Langton 3301fad8cd test: migrate app runtime logger effect tests (#27176) 2026-05-12 23:47:49 +00:00
Kit Langton 9c54255aeb fix: migrate sync http api test to effect runner (#27175) 2026-05-12 23:47:14 +00:00
Kit Langton b0674b473f test: migrate session action route to effect runner (#27174) 2026-05-12 23:46:51 +00:00
Kit Langton ded4da735b test: migrate webfetch tool tests to effect runner (#27171) 2026-05-12 23:46:23 +00:00
Kit Langton 2c334d9242 Migrate schema error body tests to Effect runner (#27172) 2026-05-12 23:46:19 +00:00
Kit Langton 81dd46abec test: migrate websearch tests to effect runner (#27170) 2026-05-12 23:45:01 +00:00
opencode-agent[bot] baef5cd43b chore: generate 2026-05-12 22:11:13 +00:00
Andrew Suffield 65368f609d fix: preserve permission ordering by accepting a layered array (#23214)
Co-authored-by: Andrew Suffield <asuffield@cloudflare.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-05-12 17:09:06 -05:00
opencode-agent[bot] 2cb697b720 chore: generate 2026-05-12 21:24:39 +00:00
Aiden Cline cb511f78ff fix(plugin): preserve tool attachments (#27157) 2026-05-12 16:23:15 -05:00
Musa 159964b172 feat(plugin): add DigitalOcean OAuth + Inference Routers (#26095) 2026-05-12 16:22:40 -05:00
Kit Langton d9f9f1553b test: use Effect file services in migrated tests (#27154) 2026-05-12 20:48:07 +00:00
Kit Langton 0fb55b4f1a test(project): migrate global project tests to Effect runner (#27142) 2026-05-12 20:45:39 +00:00
Kit Langton 3c34f6704b test: migrate auth override plugin test (#27140) 2026-05-12 20:41:34 +00:00
Kit Langton 4cf088ae84 test: migrate instance bootstrap to Effect runner (#27144) 2026-05-12 20:34:12 +00:00
Aiden Cline ca28dd02ec fix(compaction): restore tail turns after summarization (#27145) 2026-05-12 15:33:16 -05:00
Kit Langton 2017dc165c test: migrate negative tokens regression to Effect runner (#27141) 2026-05-12 20:32:23 +00:00
Kit Langton dc9d6a08cb test: migrate agent color config tests (#27139) 2026-05-12 20:31:38 +00:00
Kit Langton af4ab017cb test(session): migrate structured output integration test (#27143) 2026-05-12 20:30:35 +00:00
Kit Langton dd14413a64 Preserve native LLM tool context (#27116) 2026-05-12 16:16:58 -04:00
Frank b9e7cbf13c sync 2026-05-12 16:06:19 -04:00
Kit Langton 3f74abc6cd test: simplify Effect migration follow-ups (#27136) 2026-05-12 20:00:54 +00:00
Kit Langton e0d0fe1ff7 test(bus): migrate integration tests to Effect runner (#27132) 2026-05-12 19:58:54 +00:00
opencode-agent[bot] f7dbb4dac4 chore: generate 2026-05-12 19:48:35 +00:00
Kit Langton c5849e56cc test(project): migrate project tests to Effect runner (#27134) 2026-05-12 19:47:17 +00:00
opencode-agent[bot] e46ab34d27 chore: generate 2026-05-12 19:44:26 +00:00
Kit Langton 1d4613006a test(project): migrate instance tests to Effect runner (#27130) 2026-05-12 19:41:46 +00:00
Kit Langton 71040c54aa test(plugin): migrate loader shared tests to Effect runner (#27129) 2026-05-12 19:41:44 +00:00
Kit Langton fec78154b5 test(bus): migrate bus tests to Effect runner (#27131) 2026-05-12 19:41:24 +00:00
Kit Langton 3e2ec192cf test(question): remove WithInstance bridge (#27128) 2026-05-12 19:40:01 +00:00
Kit Langton ec960da42a test(skill): migrate discovery tests to Effect runner (#27127) 2026-05-12 19:39:03 +00:00
Shoubhit Dash 45de4975de refactor(core): resolve default agent info (#27125) 2026-05-13 01:08:30 +05:30
Kit Langton e540daabc4 test(agent): migrate plan bypass tests to Effect runner (#27119) 2026-05-12 14:56:01 -04:00
opencode-agent[bot] f3c91c5f96 chore: generate 2026-05-12 18:52:25 +00:00
Kit Langton 549b146ea6 Stabilize session event tests (#27117) 2026-05-12 18:51:18 +00:00
Kit Langton 3c7569d852 test(tool): migrate external directory tests to Effect runner (#27122) 2026-05-12 18:50:56 +00:00
Kit Langton 8f1ded9e08 test(file): migrate ripgrep tests to Effect runner (#27120) 2026-05-12 18:50:26 +00:00
Kit Langton b668af29dd test(git): migrate git tests to Effect runner (#27121) 2026-05-12 18:50:07 +00:00
Kit Langton ec30ff9120 test(agent): migrate agent tests to Effect runner (#27118) 2026-05-12 18:49:30 +00:00
opencode-agent[bot] a3714d4399 chore: update nix node_modules hashes 2026-05-12 18:27:42 +00:00
Kit Langton 822eec0d62 Fix runner cancel completion (#27115) 2026-05-12 14:22:56 -04:00
Kit Langton 3974520742 Migrate UI cancel error to tagged error (#27112) 2026-05-12 18:09:00 +00:00
Kit Langton fda37b3609 Remove Zod from app global SDK (#27111) 2026-05-12 14:02:12 -04:00
Kit Langton 6b950b666a Remove Zod from core dependencies (#27107) 2026-05-12 13:51:08 -04:00
Kit Langton bc4fdb8370 Remove unused app ID schema (#27105) 2026-05-12 13:09:23 -04:00
Kit Langton 2b9af91568 Remove Zod from core log (#27102) 2026-05-12 13:08:57 -04:00
Kit Langton 53a3f95088 Make core fn Zod import type-only (#27103) 2026-05-12 12:58:50 -04:00
Dax Raad 5a4596c879 core: Wait 3 days before installing new package versions to reduce supply chain risk 2026-05-12 12:50:32 -04:00
opencode-agent[bot] 0ce614a280 chore: generate 2026-05-12 16:46:35 +00:00
Kit Langton e8125e9b42 test(server): migrate session list tests to Effect runner (#27101) 2026-05-12 16:45:05 +00:00
Kit Langton a7b5041674 test(file): migrate fsmonitor tests to Effect runner (#27099) 2026-05-12 16:44:04 +00:00
Kit Langton a16789dfdd test(tool): migrate apply patch tests to Effect runner (#27100) 2026-05-12 16:43:33 +00:00
Kit Langton 8115004c73 test(file): migrate path traversal tests to Effect runner (#27098) 2026-05-12 16:42:49 +00:00
Kit Langton ec4fdaf8e9 test(tool): migrate tool define tests to Effect runner (#27097) 2026-05-12 16:42:19 +00:00
Shoubhit Dash 3dc2c1d81c fix(session): preserve usage update timestamps (#27094) 2026-05-12 22:10:28 +05:30
Kit Langton d658e1e350 Remove local MCP Zod schema (#27095) 2026-05-12 12:38:27 -04:00
opencode-agent[bot] 30e3fa1de9 chore: generate 2026-05-12 16:33:06 +00:00
Aiden Cline 23f8b3eb3e fix: annotate Effect log metadata (#27093) 2026-05-12 11:31:18 -05:00
Kit Langton c7d8b0d565 Delete named schema error wrapper (#27066) 2026-05-12 12:04:28 -04:00
Kit Langton 257fcafc83 test(tool): migrate edit concurrency test (#26983) 2026-05-12 11:52:31 -04:00
Kit Langton 04aafe2bfc test(provider): migrate more config-backed cases (#27067) 2026-05-12 14:19:49 +00:00
opencode-agent[bot] 0fd0facc44 chore: generate 2026-05-12 13:31:03 +00:00
Kit Langton 0de3b67cc0 test(tool): migrate shell tests to Effect runner (#26968) 2026-05-12 13:28:46 +00:00
228 changed files with 127625 additions and 73332 deletions
+10 -2
View File
@@ -33,8 +33,9 @@ runs:
shell: bash
run: echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT"
- name: Cache Bun dependencies
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
- name: Restore Bun dependencies
id: bun-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ steps.cache.outputs.dir }}
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
@@ -56,3 +57,10 @@ runs:
bun install ${{ inputs.install-flags }}
fi
shell: bash
- name: Save Bun dependencies
if: steps.bun-cache.outputs.cache-hit != 'true' && github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ steps.cache.outputs.dir }}
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
+28 -372
View File
@@ -65,7 +65,6 @@
"solid-list": "catalog:",
"tailwindcss": "catalog:",
"virtua": "catalog:",
"zod": "catalog:",
},
"devDependencies": {
"@happy-dom/global-registrator": "20.0.11",
@@ -214,7 +213,6 @@
"npm-package-arg": "13.0.2",
"semver": "^7.6.3",
"xdg-basedir": "5.1.0",
"zod": "catalog:",
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
@@ -396,10 +394,10 @@
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
"@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@openrouter/ai-sdk-provider": "2.8.1",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "2.6.1",
@@ -421,7 +419,6 @@
"bonjour-service": "1.3.0",
"bun-pty": "0.4.8",
"chokidar": "4.0.3",
"cli-sound": "1.1.3",
"clipboardy": "4.0.0",
"cross-spawn": "catalog:",
"decimal.js": "10.5.0",
@@ -511,9 +508,9 @@
"typescript": "catalog:",
},
"peerDependencies": {
"@opentui/core": ">=0.2.6",
"@opentui/keymap": ">=0.2.6",
"@opentui/solid": ">=0.2.6",
"@opentui/core": ">=0.2.8",
"@opentui/keymap": ">=0.2.8",
"@opentui/solid": ">=0.2.8",
},
"optionalPeers": [
"@opentui/core",
@@ -680,6 +677,9 @@
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
},
"overrides": {
"@opentui/core": "catalog:",
"@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:",
"@types/bun": "catalog:",
"@types/node": "catalog:",
},
@@ -693,9 +693,9 @@
"@npmcli/arborist": "9.4.0",
"@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opentui/core": "0.2.6",
"@opentui/keymap": "0.2.6",
"@opentui/solid": "0.2.6",
"@opentui/core": "0.2.8",
"@opentui/keymap": "0.2.8",
"@opentui/solid": "0.2.8",
"@pierre/diffs": "1.1.0-beta.18",
"@playwright/test": "1.59.1",
"@sentry/solid": "10.36.0",
@@ -1074,8 +1074,6 @@
"@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="],
"@dimforge/rapier2d-simd-compat": ["@dimforge/rapier2d-simd-compat@0.17.3", "", {}, "sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg=="],
"@dot/log": ["@dot/log@0.1.5", "", { "dependencies": { "chalk": "^4.1.2", "loglevelnext": "^6.0.0", "p-defer": "^3.0.0" } }, "sha512-ECraEVJWv2f2mWK93lYiefUkphStVlKD6yKDzisuoEmxuLKrxO9iGetHK2DoEAkj7sxjE886n0OUVVCUx0YPNg=="],
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
@@ -1292,62 +1290,6 @@
"@isaacs/string-locale-compare": ["@isaacs/string-locale-compare@1.1.0", "", {}, "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ=="],
"@jimp/core": ["@jimp/core@1.6.0", "", { "dependencies": { "@jimp/file-ops": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^16.0.0", "mime": "3" } }, "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w=="],
"@jimp/diff": ["@jimp/diff@1.6.0", "", { "dependencies": { "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "pixelmatch": "^5.3.0" } }, "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw=="],
"@jimp/file-ops": ["@jimp/file-ops@1.6.0", "", {}, "sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ=="],
"@jimp/js-bmp": ["@jimp/js-bmp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "bmp-ts": "^1.0.9" } }, "sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw=="],
"@jimp/js-gif": ["@jimp/js-gif@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "gifwrap": "^0.10.1", "omggif": "^1.0.10" } }, "sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g=="],
"@jimp/js-jpeg": ["@jimp/js-jpeg@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "jpeg-js": "^0.4.4" } }, "sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA=="],
"@jimp/js-png": ["@jimp/js-png@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "pngjs": "^7.0.0" } }, "sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg=="],
"@jimp/js-tiff": ["@jimp/js-tiff@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "utif2": "^4.1.0" } }, "sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw=="],
"@jimp/plugin-blit": ["@jimp/plugin-blit@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA=="],
"@jimp/plugin-blur": ["@jimp/plugin-blur@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw=="],
"@jimp/plugin-circle": ["@jimp/plugin-circle@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw=="],
"@jimp/plugin-color": ["@jimp/plugin-color@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "tinycolor2": "^1.6.0", "zod": "^3.23.8" } }, "sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA=="],
"@jimp/plugin-contain": ["@jimp/plugin-contain@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ=="],
"@jimp/plugin-cover": ["@jimp/plugin-cover@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA=="],
"@jimp/plugin-crop": ["@jimp/plugin-crop@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang=="],
"@jimp/plugin-displace": ["@jimp/plugin-displace@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q=="],
"@jimp/plugin-dither": ["@jimp/plugin-dither@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0" } }, "sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ=="],
"@jimp/plugin-fisheye": ["@jimp/plugin-fisheye@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA=="],
"@jimp/plugin-flip": ["@jimp/plugin-flip@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg=="],
"@jimp/plugin-hash": ["@jimp/plugin-hash@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "any-base": "^1.1.0" } }, "sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q=="],
"@jimp/plugin-mask": ["@jimp/plugin-mask@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA=="],
"@jimp/plugin-print": ["@jimp/plugin-print@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/types": "1.6.0", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", "simple-xml-to-json": "^1.2.2", "zod": "^3.23.8" } }, "sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A=="],
"@jimp/plugin-quantize": ["@jimp/plugin-quantize@1.6.0", "", { "dependencies": { "image-q": "^4.0.0", "zod": "^3.23.8" } }, "sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg=="],
"@jimp/plugin-resize": ["@jimp/plugin-resize@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA=="],
"@jimp/plugin-rotate": ["@jimp/plugin-rotate@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw=="],
"@jimp/plugin-threshold": ["@jimp/plugin-threshold@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w=="],
"@jimp/types": ["@jimp/types@1.6.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg=="],
"@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="],
"@joshwooding/vite-plugin-react-docgen-typescript": ["@joshwooding/vite-plugin-react-docgen-typescript@0.7.0", "", { "dependencies": { "glob": "^13.0.1", "react-docgen-typescript": "^2.2.2" }, "peerDependencies": { "typescript": ">= 4.3.x", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
@@ -1620,23 +1562,23 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
"@opentui/core": ["@opentui/core@0.2.6", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.6", "@opentui/core-darwin-x64": "0.2.6", "@opentui/core-linux-arm64": "0.2.6", "@opentui/core-linux-x64": "0.2.6", "@opentui/core-win32-arm64": "0.2.6", "@opentui/core-win32-x64": "0.2.6" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-dBpMaWVM7wtW2/2TlGPrkPjg6gOL3MVU/5XXk+U1LDJB8L4q4NeYWVdzfAVNcEvgmuuCy/cVqdY2D4ei+e7MMg=="],
"@opentui/core": ["@opentui/core@0.2.8", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.8", "@opentui/core-darwin-x64": "0.2.8", "@opentui/core-linux-arm64": "0.2.8", "@opentui/core-linux-x64": "0.2.8", "@opentui/core-win32-arm64": "0.2.8", "@opentui/core-win32-x64": "0.2.8" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-bRRiCXuwjS8/6mN1oA5iVaf55z9APyalm7FnoxkLkEyIU1VDaQeTpYtElBbfo1rxtcO6Rj53XywH9oW8auNO9A=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hR5nsxNj+059utzenTCF0kealUlibON6fLuebFUCGM/5kJnqa+shIh0XbUDFm0+F47vqVUgZufBdUuieQZIbvQ=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Qh6VCMQgW3hWh/7MR51y+XuQezh8NOLwKS8EQSoKzAr4VOc/W5P0/DvgMKgwaqXw2Mz0AIba/BvZ6by20yc4zA=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-pJ/bH4WC/mbBaakM1YdH6TVo67jhy0KPd61bCz97w0I/PJGr8fmNKvhmMt/AwyFgOQi3FYZiEKLMpGdvUcSsrQ=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-wQjJ38C3IiVx/gwwBYxnCarzgD75FdS7IyUErt3lhn57XriNiCbb7ScphWnRMwwtL8CI+bBGzClroDRA2lCfvg=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Pnd3kOxig8ii+/IqYheOPEgferylsQA0L6tKBnHQ9jRlCJOcu0Rv65Jepueh212vevdV9DzPURJnhejG06J6g=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-fx4ADeWSSSVU1O/MkMnklCRxtWRy6CLeAvktLlNdPb+BhmQIDg1kpZcdv7m/3cgD1/ksFEXIwO6VTvfKYE0umw=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-458Mx9tBzEPzfft8cSt5ZaIpEepoxBXBOL6AUVmDTKWaZ3uouraPcEKraGAyvOTDQp2XDI3R8c/2GdaR77FaUQ=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.8", "", { "os": "linux", "cpu": "x64" }, "sha512-4ekUyzopBj2ClsUbneLnUOrmZtvU67FCVFLgmBfKL4IvVl/P0YobGNg71gN1JNiYpY7hK77qOpidVLHcNMIE7w=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-BDUrdrT1RCcVnQoHJmUut4y811jDBAEtc6GJFB4Gs265Be8SrTjVCus6p2fSQ7j9sZQ1OcjO+5+4NkheSZICDQ=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-63K046wpzTzQOLOG9LTsp3+Ld0TNTxeQczexkg0pKSBxZFhws+/9YIGjTctZmJUfE1g1X4tI31dO+KNRpXRHQw=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-SUYAzRJ9TSoD2Qt8kn6FJz6dbTrFEPVig5mScB4zFGgGQO/Bbod2/Q31vLS/IQrX+FDb67WaErD+kuMCnMPPLA=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.8", "", { "os": "win32", "cpu": "x64" }, "sha512-+WDiTlTyDpgkis8rPAhW1fS7TwXJih+fk+RYXS2bC3tAKsRD+O3PRSkVABRbjkuXbtfJZf2cjOHZFGN4Vf5qDg=="],
"@opentui/keymap": ["@opentui/keymap@0.2.6", "", { "dependencies": { "@opentui/core": "0.2.6" }, "peerDependencies": { "@opentui/react": "0.2.6", "@opentui/solid": "0.2.6", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-+6OYuedrFCKVo4ryGFNwws++2VOmPcXU3PwpY0mP47gYQY2nvQ+etWIs2Y7r5eMIqUfxVCldkKsrzcEcA4tb/A=="],
"@opentui/keymap": ["@opentui/keymap@0.2.8", "", { "dependencies": { "@opentui/core": "0.2.8" }, "peerDependencies": { "@opentui/react": "0.2.8", "@opentui/solid": "0.2.8", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-/H9j8fP64cf3/nFDCvVP8+7cwU/oRh4sgfQH2NhcPp8illgBb/e9pG5x3vM0nK4RVyTqUvkPXsOeIX5u7vltlg=="],
"@opentui/solid": ["@opentui/solid@0.2.6", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.6", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-2y225WlOGi/fCaajkxBmLyVW8Cr+OmhowHdvrYcz5w2kBD15sKbJLIYu1G9DxceirT1uIyasGy2TGzRRcVkTDg=="],
"@opentui/solid": ["@opentui/solid@0.2.8", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.8", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-f2g0riBuzk4/ZmcJnp1k13odUmNZcfA3nF7RzdSlEfpkwNDfc4xqnRAwYbNNDwGNrJX0JDCTEZY5ZEhuL155MQ=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -2288,8 +2230,6 @@
"@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@tsconfig/bun": ["@tsconfig/bun@1.0.9", "", {}, "sha512-4M0/Ivfwcpz325z6CwSifOBZYji3DFOEpY6zEUt0+Xi2qRhzwvmqQN9XAHJh3OVvRJuAqVTLU2abdCplvp6mwQ=="],
"@tsconfig/node22": ["@tsconfig/node22@22.0.2", "", {}, "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA=="],
@@ -2554,8 +2494,6 @@
"ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
"any-base": ["any-base@1.1.0", "", {}, "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="],
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
@@ -2624,8 +2562,6 @@
"avvio": ["avvio@9.2.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ=="],
"await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="],
"aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="],
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
@@ -2690,8 +2626,6 @@
"blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="],
"bmp-ts": ["bmp-ts@1.0.9", "", {}, "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw=="],
"body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="],
"bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="],
@@ -2734,16 +2668,6 @@
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
"bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
"bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lIsDkPzJzPl6yrB5CUOINJFPnTRv6fF/Q8J1mAr43ogSp86WZEg9XZKaT6f3EUJ+9ETogGoMnoj1q0AwHUTbAQ=="],
"bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-uEddf5U7GvKIkM/BV18rUKtYHL6d0KeqBjNHwfqDH9QgEo9KVSKvJXS5I/sMefk5V5pIYE+8tQhtrREevhocng=="],
"bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-Y/f15j9r8ba0xUz+3lATtS74OE+PPzQXO7Do/1eCluJcuOlfa77kMjvBK/ShWnem3Y9xqi59pebTPOGRB+CaJA=="],
"bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-MHSFAKqizISb+C5NfDrFe3g0Al5Njnu0j/A+oO2Q+bIWX+fUYjBSowiYE1ZXJx65KuryuB+tiM7Qh6cQbVvkEg=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@@ -2814,8 +2738,6 @@
"cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
"cli-sound": ["cli-sound@1.1.3", "", { "dependencies": { "find-exec": "^1.0.3" }, "bin": { "cli-sound": "dist/esm/cli.js" } }, "sha512-dpdF3KS3wjo1fobKG5iU9KyKqzQWAqueymHzZ9epus/dZ40487gAvS6aXFeBul+GiQAQYUTAtUWgQvw6Jftbyg=="],
"cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
@@ -3166,8 +3088,6 @@
"execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
"exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="],
"exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
@@ -3230,8 +3150,6 @@
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
"filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@@ -3240,8 +3158,6 @@
"find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
"find-exec": ["find-exec@1.0.3", "", { "dependencies": { "shell-quote": "^1.8.1" } }, "sha512-gnG38zW90mS8hm5smNcrBnakPEt+cGJoiMkJwCU0IYnEb0H2NQk0NIljhNW+48oniCriFek/PH6QXbwsJo/qug=="],
"find-my-way": ["find-my-way@9.5.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ=="],
"find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
@@ -3324,8 +3240,6 @@
"ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#20bd361", {}, "anomalyco-ghostty-web-20bd361", "sha512-dW0nwaiBBcun9y5WJSvm3HxDLe5o9V0xLCndQvWonRVubU8CS1PHxZpLffyPt1YujPWC13ez03aWxcuKBPYYGQ=="],
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
@@ -3478,8 +3392,6 @@
"ignore-walk": ["ignore-walk@8.0.0", "", { "dependencies": { "minimatch": "^10.0.3" } }, "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A=="],
"image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="],
"immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
"import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="],
@@ -3622,16 +3534,12 @@
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
"jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jmespath": ["jmespath@0.16.0", "", {}, "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="],
"jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
"jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
"js-base64": ["js-base64@3.7.7", "", {}, "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="],
"js-beautify": ["js-beautify@1.15.4", "", { "dependencies": { "config-chain": "^1.1.13", "editorconfig": "^1.0.4", "glob": "^10.4.2", "js-cookie": "^3.0.5", "nopt": "^7.2.1" }, "bin": { "css-beautify": "js/bin/css-beautify.js", "html-beautify": "js/bin/html-beautify.js", "js-beautify": "js/bin/js-beautify.js" } }, "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA=="],
@@ -4092,8 +4000,6 @@
"oidc-token-hash": ["oidc-token-hash@5.2.0", "", {}, "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw=="],
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
@@ -4168,12 +4074,6 @@
"param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="],
"parse-bmfont-ascii": ["parse-bmfont-ascii@1.0.6", "", {}, "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA=="],
"parse-bmfont-binary": ["parse-bmfont-binary@1.0.6", "", {}, "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA=="],
"parse-bmfont-xml": ["parse-bmfont-xml@1.1.6", "", { "dependencies": { "xml-parse-from-string": "^1.0.0", "xml2js": "^0.5.0" } }, "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA=="],
"parse-conflict-json": ["parse-conflict-json@5.0.1", "", { "dependencies": { "json-parse-even-better-errors": "^5.0.0", "just-diff": "^6.0.0", "just-diff-apply": "^5.2.0" } }, "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ=="],
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
@@ -4218,8 +4118,6 @@
"peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="],
"peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="],
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
"perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="],
@@ -4240,8 +4138,6 @@
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
"pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="],
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
"pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="],
@@ -4250,16 +4146,12 @@
"pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="],
"planck": ["planck@1.5.0", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-dlvqJE+FscZgrGUXJ5ybd0o5bvZ5XXyZNbm08xGsXp9WjXeAyWSFT6n9s/1PQcUBo4546fDXA5RMA4wbDyZw6g=="],
"playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="],
"playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
"plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="],
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
"poe-oauth": ["poe-oauth@0.0.6", "", {}, "sha512-dI8xrVl7RSFh0B+cb4GGuCjIfGtDT9VpbpVkP0UKcunpXF0eFw+6GencoJ7k+E02ZYqopBQApMVWGq70/GP69w=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
@@ -4384,8 +4276,6 @@
"readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
"readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="],
"readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
@@ -4570,8 +4460,6 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
"shiki": ["shiki@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/engine-javascript": "3.20.0", "@shikijs/engine-oniguruma": "3.20.0", "@shikijs/langs": "3.20.0", "@shikijs/themes": "3.20.0", "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg=="],
"shikiji": ["shikiji@0.6.13", "", { "dependencies": { "hast-util-to-html": "^9.0.0" } }, "sha512-4T7X39csvhT0p7GDnq9vysWddf2b6BeioiN3Ymhnt3xcy9tXmDcnsEFVxX18Z4YcQgEE/w48dLJ4pPPUcG9KkA=="],
@@ -4594,8 +4482,6 @@
"simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="],
"simple-xml-to-json": ["simple-xml-to-json@1.2.7", "", {}, "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"sitemap": ["sitemap@9.0.1", "", { "dependencies": { "@types/node": "^24.9.2", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/esm/cli.js" } }, "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ=="],
@@ -4682,8 +4568,6 @@
"stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],
"stage-js": ["stage-js@1.0.2", "", {}, "sha512-EWTRBYlg7Qv9wGUao99/PfRe3KaiQqWmgSvTOXvaWnu1Jk/q/vV8yJVu6bi/3EqDZeMVnCPAjheba6OFc5k1GQ=="],
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
@@ -4732,8 +4616,6 @@
"strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
"strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="],
"stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="],
"stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="],
@@ -4786,8 +4668,6 @@
"thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="],
"three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="],
"thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="],
"tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="],
@@ -4800,8 +4680,6 @@
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
@@ -4822,8 +4700,6 @@
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="],
"toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="],
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.8", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-b+5ynEFp4Woe5a22hzNQm42lD23t13ZMihVxHbzjA50zdcM9aOSJTIjdJ0PDSd4/50HbBXcpHiQsz6rM4N88ww=="],
@@ -4976,8 +4852,6 @@
"utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
"utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="],
"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=="],
@@ -5106,8 +4980,6 @@
"xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="],
"xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="],
"xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
@@ -5460,46 +5332,10 @@
"@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"@happy-dom/global-registrator/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
"@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"@jimp/core/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
"@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-circle/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-color/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-contain/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-cover/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-crop/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-displace/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-fisheye/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-flip/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-mask/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-print/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-quantize/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-resize/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-rotate/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-threshold/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/types/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jsx-email/cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@jsx-email/cli/esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="],
@@ -5638,24 +5474,16 @@
"@slack/bolt/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
"@slack/logger/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="],
"@slack/oauth/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@slack/socket-mode/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="],
"@slack/socket-mode/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@slack/socket-mode/@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="],
"@slack/socket-mode/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
"@slack/web-api/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="],
"@slack/web-api/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@slack/web-api/eventemitter3": ["eventemitter3@3.1.2", "", {}, "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="],
"@slack/web-api/form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="],
@@ -5716,62 +5544,8 @@
"@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
"@types/body-parser/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/cacache/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/cacheable-request/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/connect/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/cross-spawn/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/express-serve-static-core/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/fontkit/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/fs-extra/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/is-stream/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/jsonwebtoken/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/keyv/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/mssql/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/node-fetch/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/npm-registry-fetch/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/npmcli__arborist/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/npmlog/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/pacote/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/plist/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
"@types/readable-stream/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/responselike/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/sax/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/send/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/serve-static/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/ssri/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/tunnel/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/ws/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/yauzl/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
"@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
@@ -5848,18 +5622,12 @@
"builder-util-runtime/sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
"bun-types/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="],
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
"cloudflare/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"condense-newlines/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="],
@@ -5894,8 +5662,6 @@
"effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"electron/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"electron-builder/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"electron-builder/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
@@ -5950,8 +5716,6 @@
"gray-matter/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
"happy-dom/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"happy-dom/ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
"html-minifier-terser/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
@@ -5964,8 +5728,6 @@
"iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="],
"image-q/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"js-beautify/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"js-beautify/nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="],
@@ -6038,9 +5800,9 @@
"openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"opentui-spinner/@opentui/core": ["@opentui/core@0.1.105", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.105", "@opentui/core-darwin-x64": "0.1.105", "@opentui/core-linux-arm64": "0.1.105", "@opentui/core-linux-x64": "0.1.105", "@opentui/core-win32-arm64": "0.1.105", "@opentui/core-win32-x64": "0.1.105", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-vllSOOCW6VIThV/96GRLJ1IxIBuR+ci6FDvnPIAG4s7SJ/FW6zAkqDn1xrtBwwk/lM3QWjLqy8BZc+zwWvveJA=="],
"opentui-spinner/@opentui/core": ["@opentui/core@0.2.7", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.7", "@opentui/core-darwin-x64": "0.2.7", "@opentui/core-linux-arm64": "0.2.7", "@opentui/core-linux-x64": "0.2.7", "@opentui/core-win32-arm64": "0.2.7", "@opentui/core-win32-x64": "0.2.7" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-cnN6JcaGC7SeQzobBy/CHzqUAQFtypazuw1CjQBo7WwoOiLMGubt9W5FXeF0zIrSxH2Ed6NLWhPYRg7SD4629Q=="],
"opentui-spinner/@opentui/solid": ["@opentui/solid@0.1.105", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.105", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-uxnaMP802sCI487pv/Hk9xdFdIj9mkg3eNliAqbqR0Shmd4phcjKEZvPRpijjmI99j4s9nul71jzF3h1oz31Nw=="],
"opentui-spinner/@opentui/solid": ["@opentui/solid@0.2.7", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.7", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-nlkx9HvuWaHtc5A8eUEAPNi+5+37LZS3ln73WRmtT5xin8LnQf+yhwopqGgPSnLq1ODLwhkKRdr/9JCDr2j7Bg=="],
"ora/bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
@@ -6054,14 +5816,10 @@
"p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
"pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
@@ -6086,8 +5844,6 @@
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"protobufjs/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
@@ -6118,8 +5874,6 @@
"shiki/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
"sitemap/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"sitemap/sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
"slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
@@ -6142,20 +5896,14 @@
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"stripe/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
"tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"tedious/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
"token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"tree-sitter-bash/node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="],
"tw-to-css/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
@@ -6172,8 +5920,6 @@
"uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
@@ -6488,8 +6234,6 @@
"@gitlab/opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
"@happy-dom/global-registrator/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
"@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="],
@@ -6684,14 +6428,6 @@
"@sentry/cli/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"@slack/logger/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@slack/oauth/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@slack/socket-mode/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@slack/web-api/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"@slack/web-api/p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
@@ -6718,60 +6454,6 @@
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/body-parser/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/cacache/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/cacheable-request/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/connect/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/cross-spawn/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/express-serve-static-core/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/fontkit/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/fs-extra/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/is-stream/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/jsonwebtoken/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/keyv/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/mssql/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/node-fetch/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/npm-registry-fetch/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/npmcli__arborist/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/npmlog/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/pacote/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/plist/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/readable-stream/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/responselike/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/sax/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/send/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/serve-static/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/ssri/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/tunnel/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/ws/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/yauzl/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
@@ -6820,12 +6502,8 @@
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"bun-types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"cloudflare/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"crc/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
@@ -6844,8 +6522,6 @@
"electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"electron/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
@@ -6858,14 +6534,10 @@
"gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"happy-dom/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"iconv-corefoundation/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
"iconv-corefoundation/cli-truncate/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"image-q/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"js-beautify/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
@@ -6884,8 +6556,6 @@
"motion/framer-motion/motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="],
"mssql/tedious/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"mssql/tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
@@ -6904,24 +6574,22 @@
"opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
"opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.105", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1pIL7aer9amwj8EpYoMNtvavKetIe+nX8uBRmYsMQb+KvJoUAZUqENfRW+qHE5WrsOyxx8/QoyXTHw15GG5iLQ=="],
"opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CAy6cL3byz2Xf6gFiJHBpcnsp/2ADEWLLOUokVypOyPLcy8GY3sPzlA4pkAjVGQMYQhDj+Y3+SXz4uTLt4AETg=="],
"opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.105", "", { "os": "darwin", "cpu": "x64" }, "sha512-hLIRSWlK3gY2NRXJGWiTBiMYSmRDjOYFZF6WtUVXhY2SL3sp08dhmr/6dmAVH+3pKCsCipLEsrrcQX6SAihCTA=="],
"opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-K06h333rMkC9cyMJr/VvcRK3ik81Admd8ZsES5uf5YXWPdYhXGf75I1T8mKIThhUmoFLb8R5xqfuPmoocsjM7Q=="],
"opentui-spinner/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.105", "", { "os": "linux", "cpu": "arm64" }, "sha512-jlRKfPkozTZEkHEePuCWYcTIUtPm+ieInAwGVqGmjbvqjxdVv1/W/Dt6LEZ/9jpRiOPd+FjXAfLe6wa/XWHr+w=="],
"opentui-spinner/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-iYWGTztbdG9yYSB5Alxuo0dWAmkWQR0+/paNWUyPOocjigmKgMmACDtHgYqa7sxkIcWgmXljt/f8rgXDG4wdMg=="],
"opentui-spinner/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.105", "", { "os": "linux", "cpu": "x64" }, "sha512-kfWS1WMg6qHShmxZX9s1tZc/8JcXw6uyy2UtyTbJdRFExtXGH37oKHi8QK8iPL2ExCx4z7zqVnVJfO3X/Wh7lA=="],
"opentui-spinner/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.7", "", { "os": "linux", "cpu": "x64" }, "sha512-tymBCfYbsDRfHQNXsolkFfaTEIDhemD4+1ZovUztQd7i+0Ggnu9WbPN1SNCiRz6PjrlaNeQzZE3Wl8FfVdw/cw=="],
"opentui-spinner/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.105", "", { "os": "win32", "cpu": "arm64" }, "sha512-UFx6A8OpBVbGWK6OAw4GqAqKZgIITJfSOd35pG9yDVKQouHN2OGc2HeeXrH2A4h42p40Xl6IfcqqfllkpC13Dg=="],
"opentui-spinner/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-XLPJWdT8QOukrYDkpIng6+uNUlF66ByXcQlC3qA9JbrUTBetZhgXs8Q2jEjRfc+Ty3uh1iRSA6PgJGbbOK/f4Q=="],
"opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.105", "", { "os": "win32", "cpu": "x64" }, "sha512-f9FqqUmxehwhF+cgyazm0YT0v0BYTTCPzd6eztqhl74N3x/kC+jOOz2rdJDC/tTBo1JVsF64KupOnhIs6/Cogg=="],
"opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.7", "", { "os": "win32", "cpu": "x64" }, "sha512-CzVGEfqysVk8Hxcj0RDv/DtXIM6iZmbmr23kW7y8CJMPtmV1gmKI4D9abVjynWJnGbaSBnDi43mgZnGMgOdyEg=="],
"opentui-spinner/@opentui/core/bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
"opentui-spinner/@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
"opentui-spinner/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
"opentui-spinner/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
"ora/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"ora/bl/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=="],
@@ -6930,14 +6598,10 @@
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"parse-bmfont-xml/xml2js/sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
"protobufjs/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"readable-stream/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="],
@@ -6948,16 +6612,10 @@
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"sitemap/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"stripe/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"tedious/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"tw-to-css/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"tw-to-css/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
@@ -7260,8 +6918,6 @@
"js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"mssql/tedious/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
+3 -1
View File
@@ -1,6 +1,8 @@
[install]
exact = true
# Only install newly resolved package versions published at least 3 days ago.
minimumReleaseAge = 259200
minimumReleaseAgeExcludes = ["@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-x64", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid"]
[test]
root = "./do-not-run-tests-from-root"
+9
View File
@@ -293,3 +293,12 @@ new sst.cloudflare.x.SolidStart("Console", {
},
},
})
////////////////
// HELPERS
////////////////
export const stat = new sst.cloudflare.Worker("Stat", {
handler: "packages/console/function/src/stat.ts",
link: [database],
})
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-Q9r1S15YL9LQK7DRhuOpw3Fxi24BPovEM995GZJayKw=",
"aarch64-linux": "sha256-C0rRTLnxxuuEkCBc3JZbkR66TUVwpcPFif3BU9GRAuA=",
"aarch64-darwin": "sha256-1HvalOO/pOkRlYH8CZ93psapt90C+pYzui1JCadBE1Q=",
"x86_64-darwin": "sha256-RrndyLWfhWm4mZ88XytFF2NI+ly8la550Z5LBN/g5u4="
"x86_64-linux": "sha256-CF+ecq3PqUuA7ta09oYkDaFz+FxZqQragu9xvtw++RE=",
"aarch64-linux": "sha256-QVdgdCncl+XEIx569oNNRXyxRh5mkEMJSTSO1I3QfzM=",
"aarch64-darwin": "sha256-dfP1SEpgQ63DEfG4eRD7BWGoMPFmjGSWiW4M0J4Asfg=",
"x86_64-darwin": "sha256-JgXg+BwZTWroChnxE6akoEFfdb7vVH0qBMZSZ7TJhDU="
}
}
+6 -3
View File
@@ -35,9 +35,9 @@
"@types/cross-spawn": "6.0.6",
"@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2",
"@opentui/core": "0.2.6",
"@opentui/keymap": "0.2.6",
"@opentui/solid": "0.2.6",
"@opentui/core": "0.2.8",
"@opentui/keymap": "0.2.8",
"@opentui/solid": "0.2.8",
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
"@types/luxon": "3.7.1",
@@ -128,6 +128,9 @@
"electron"
],
"overrides": {
"@opentui/core": "catalog:",
"@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:",
"@types/bun": "catalog:",
"@types/node": "catalog:"
},
+1 -2
View File
@@ -73,7 +73,6 @@
"solid-js": "catalog:",
"solid-list": "catalog:",
"tailwindcss": "catalog:",
"virtua": "catalog:",
"zod": "catalog:"
"virtua": "catalog:"
}
}
+3 -5
View File
@@ -3,15 +3,13 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { makeEventListener } from "@solid-primitives/event-listener"
import { batch, onCleanup, onMount } from "solid-js"
import z from "zod"
import { createSdkForServer } from "@/utils/server"
import { useLanguage } from "./language"
import { usePlatform } from "./platform"
import { useServer } from "./server"
const abortError = z.object({
name: z.literal("AbortError"),
})
const isAbortError = (error: unknown) =>
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
name: "GlobalSDK",
@@ -103,7 +101,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
let streamErrorLogged = false
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
const aborted = (error: unknown) => abortError.safeParse(error).success
const aborted = isAbortError
let attempt: AbortController | undefined
let run: Promise<void> | undefined
@@ -166,18 +166,9 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
})
const isWorking = createMemo(() => {
if (hasPermissions()) return false
const pending = (sessionStore.message[props.session.id] ?? []).findLast(
(message) =>
message.role === "assistant" &&
typeof (message as { time?: { completed?: unknown } }).time?.completed !== "number",
)
// This matches how the TUI does it
const status = sessionStore.session_status[props.session.id]
return (
pending !== undefined ||
status?.type === "busy" ||
status?.type === "retry" ||
(status !== undefined && status.type !== "idle")
)
return status?.type === "busy" || status?.type === "retry"
})
const tint = createMemo(() => messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent))
+2 -4
View File
@@ -1497,10 +1497,8 @@ export default function Page() {
})
const busy = (sessionID: string) => {
if ((sync.data.session_status[sessionID] ?? { type: "idle" as const }).type !== "idle") return true
return (sync.data.message[sessionID] ?? []).some(
(item) => item.role === "assistant" && typeof item.time.completed !== "number",
)
// This matches how the TUI does it
return (sync.data.session_status[sessionID] ?? { type: "idle" as const }).type !== "idle"
}
const queuedFollowups = createMemo(() => {
-6
View File
@@ -1,5 +1,3 @@
import z from "zod"
const prefixes = {
session: "ses",
message: "msg",
@@ -15,10 +13,6 @@ let counter = 0
type Prefix = keyof typeof prefixes
export namespace Identifier {
export function schema(prefix: Prefix) {
return z.string().startsWith(prefixes[prefix])
}
export function ascending(prefix: Prefix, given?: string) {
return generateID(prefix, false, given)
}
@@ -299,7 +299,6 @@ export async function handler(
let buffer = ""
let responseLength = 0
let timestampFirstByte = 0
let timestampLastByte = 0
function pump(): Promise<void> {
return (
+1
View File
@@ -298,6 +298,7 @@ declare module "sst" {
"EnterpriseStorage": cloudflare.R2Bucket
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"Stat": cloudflare.Service
"ZenData": cloudflare.R2Bucket
"ZenDataNew": cloudflare.R2Bucket
}
+39
View File
@@ -0,0 +1,39 @@
import { WorkerEntrypoint } from "cloudflare:workers"
import { and, Database, inArray } from "@opencode-ai/console-core/drizzle/index.js"
import { ModelTpsRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
type Result = Record<string, { qualify: number; unqualify: number }>
export default class Stat extends WorkerEntrypoint {
async fetch() {
return new Response("Not Found", { status: 404 })
}
async getStats(ids: string[]): Promise<Result> {
if (ids.length === 0) return {}
const toInterval = (date: Date) =>
parseInt(
date
.toISOString()
.replace(/[^0-9]/g, "")
.substring(0, 12),
)
const now = Date.now()
const intervals = Array.from({ length: 5 }, (_, i) => toInterval(new Date(now - i * 60 * 1000)))
const rows = await Database.use((tx) =>
tx
.select()
.from(ModelTpsRateLimitTable)
.where(and(inArray(ModelTpsRateLimitTable.id, ids), inArray(ModelTpsRateLimitTable.interval, intervals))),
)
const result: Result = Object.fromEntries(ids.map((id) => [id, { qualify: 0, unqualify: 0 }]))
for (const row of rows) {
result[row.id].qualify += row.qualify
result[row.id].unqualify += row.unqualify
}
return result
}
}
+1
View File
@@ -298,6 +298,7 @@ declare module "sst" {
"EnterpriseStorage": cloudflare.R2Bucket
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"Stat": cloudflare.Service
"ZenData": cloudflare.R2Bucket
"ZenDataNew": cloudflare.R2Bucket
}
+1
View File
@@ -298,6 +298,7 @@ declare module "sst" {
"EnterpriseStorage": cloudflare.R2Bucket
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"Stat": cloudflare.Service
"ZenData": cloudflare.R2Bucket
"ZenDataNew": cloudflare.R2Bucket
}
+1 -2
View File
@@ -41,8 +41,7 @@
"minimatch": "10.2.5",
"npm-package-arg": "13.0.2",
"semver": "^7.6.3",
"xdg-basedir": "5.1.0",
"zod": "catalog:"
"xdg-basedir": "5.1.0"
},
"overrides": {
"drizzle-orm": "catalog:"
+207
View File
@@ -0,0 +1,207 @@
import { Context, Duration, Effect, Fiber, Layer, Schema, Stream } from "effect"
import type { PlatformError } from "effect/PlatformError"
import { ChildProcess } from "effect/unstable/process"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { CrossSpawnSpawner } from "./cross-spawn-spawner"
export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()("AppProcessError", {
command: Schema.String,
exitCode: Schema.optional(Schema.Number),
stderr: Schema.optional(Schema.String),
cause: Schema.optional(Schema.Defect),
}) {}
export interface RunOptions {
readonly maxOutputBytes?: number
readonly maxErrorBytes?: number
readonly signal?: AbortSignal
readonly timeout?: Duration.Input
}
export interface RunStreamOptions {
readonly signal?: AbortSignal
readonly includeStderr?: boolean
readonly okExitCodes?: ReadonlyArray<number>
readonly maxErrorBytes?: number
}
export interface RunResult {
readonly command: string
readonly exitCode: number
readonly stdout: Buffer
readonly stderr: Buffer
readonly truncated: boolean
}
export type Interface = ChildProcessSpawner["Service"] & {
readonly run: (command: ChildProcess.Command, options?: RunOptions) => Effect.Effect<RunResult, AppProcessError>
readonly runStream: (
command: ChildProcess.Command,
options?: RunStreamOptions,
) => Stream.Stream<string, AppProcessError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/AppProcess") {}
export const requireSuccess = (result: RunResult): Effect.Effect<RunResult, AppProcessError> =>
result.exitCode === 0
? Effect.succeed(result)
: Effect.fail(
new AppProcessError({
command: result.command,
exitCode: result.exitCode,
stderr: result.stderr.toString("utf8"),
}),
)
export const requireExitIn =
(codes: ReadonlyArray<number>) =>
(result: RunResult): Effect.Effect<RunResult, AppProcessError> =>
codes.includes(result.exitCode)
? Effect.succeed(result)
: Effect.fail(
new AppProcessError({
command: result.command,
exitCode: result.exitCode,
stderr: result.stderr.toString("utf8"),
}),
)
const describeCommand = (command: ChildProcess.Command): string => {
if (command._tag === "StandardCommand") {
return command.args.length ? `${command.command} ${command.args.join(" ")}` : command.command
}
return `${describeCommand(command.left)} | ${describeCommand(command.right)}`
}
const wrapError = (description: string, cause: unknown): AppProcessError =>
cause instanceof AppProcessError ? cause : new AppProcessError({ command: description, cause })
const abortError = (signal: AbortSignal): Error => {
const reason = signal.reason
if (reason instanceof Error) return reason
const err = new Error("Aborted")
err.name = "AbortError"
return err
}
const waitForAbort = (signal: AbortSignal) =>
Effect.callback<never, Error>((resume) => {
if (signal.aborted) {
resume(Effect.fail(abortError(signal)))
return
}
const onabort = () => resume(Effect.fail(abortError(signal)))
signal.addEventListener("abort", onabort, { once: true })
return Effect.sync(() => signal.removeEventListener("abort", onabort))
})
const collectStream = (stream: Stream.Stream<Uint8Array, PlatformError>, maxOutputBytes: number | undefined) =>
Stream.runFold(
stream,
() => ({ chunks: [] as Uint8Array[], bytes: 0, truncated: false }),
(acc, chunk) => {
if (maxOutputBytes === undefined) {
acc.chunks.push(chunk)
acc.bytes += chunk.length
return acc
}
const remaining = maxOutputBytes - acc.bytes
if (remaining > 0) acc.chunks.push(remaining >= chunk.length ? chunk : chunk.slice(0, remaining))
acc.bytes += chunk.length
acc.truncated = acc.truncated || acc.bytes > maxOutputBytes
return acc
},
).pipe(Effect.map((x) => ({ buffer: Buffer.concat(x.chunks), truncated: x.truncated })))
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner
const run = Effect.fn("AppProcess.run")(function* (command: ChildProcess.Command, options?: RunOptions) {
const description = describeCommand(command)
const collect = Effect.scoped(
Effect.gen(function* () {
const handle = yield* spawner.spawn(command)
const [stdout, stderr, exitCode] = yield* Effect.all(
[
collectStream(handle.stdout, options?.maxOutputBytes),
collectStream(handle.stderr, options?.maxErrorBytes),
handle.exitCode,
],
{ concurrency: "unbounded" },
)
return {
command: description,
exitCode,
stdout: stdout.buffer,
stderr: stderr.buffer,
truncated: stdout.truncated,
} satisfies RunResult
}),
)
const timed = options?.timeout
? Effect.timeoutOrElse(collect, {
duration: options.timeout,
orElse: () => Effect.fail(new AppProcessError({ command: description, cause: new Error("Timed out") })),
})
: collect
const aborted = options?.signal
? timed.pipe(
Effect.raceFirst(
waitForAbort(options.signal).pipe(Effect.mapError((cause) => wrapError(description, cause))),
),
)
: timed
return yield* aborted.pipe(Effect.catch((cause) => Effect.fail(wrapError(description, cause))))
})
const runStream = (
command: ChildProcess.Command,
options?: RunStreamOptions,
): Stream.Stream<string, AppProcessError> => {
const description = describeCommand(command)
const okExitCodes = options?.okExitCodes
const built: Stream.Stream<string, AppProcessError | PlatformError> = Stream.unwrap(
Effect.gen(function* () {
const handle = yield* spawner.spawn(command)
const stderrFiber = yield* Effect.forkScoped(
collectStream(handle.stderr, options?.maxErrorBytes).pipe(Effect.map((x) => x.buffer.toString("utf8"))),
)
const source = options?.includeStderr === true ? handle.all : handle.stdout
const lines = source.pipe(
Stream.decodeText,
Stream.splitLines,
Stream.filter((line) => line.length > 0),
)
const tail = Stream.unwrap(
Effect.gen(function* () {
const code = yield* handle.exitCode
if (okExitCodes && okExitCodes.length > 0 && !okExitCodes.includes(code)) {
const stderr = yield* Fiber.join(stderrFiber)
return Stream.fail(new AppProcessError({ command: description, exitCode: code, stderr }))
}
return Stream.empty
}),
)
return Stream.concat(lines, tail) as Stream.Stream<string, AppProcessError | PlatformError>
}),
)
const mapped = built.pipe(
Stream.catch((cause): Stream.Stream<string, AppProcessError> => Stream.fail(wrapError(description, cause))),
)
if (!options?.signal) return mapped
const signal = options.signal
return mapped.pipe(
Stream.interruptWhen(waitForAbort(signal).pipe(Effect.mapError((cause) => wrapError(description, cause)))),
)
}
return Service.of({ ...spawner, run, runStream })
}),
)
export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
export * as AppProcess from "./process"
-11
View File
@@ -1,11 +0,0 @@
import { z } from "zod"
export function fn<T extends z.ZodType, Result>(schema: T, cb: (input: z.infer<T>) => Result) {
const result = (input: z.infer<T>) => {
const parsed = schema.parse(input)
return cb(parsed)
}
result.force = (input: z.infer<T>) => cb(input)
result.schema = schema
return result
}
+6 -3
View File
@@ -4,11 +4,14 @@ import path from "path"
import fs from "fs/promises"
import { createWriteStream } from "fs"
import * as Global from "../global"
import z from "zod"
import { Schema } from "effect"
import { Glob } from "./glob"
export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" })
export type Level = z.infer<typeof Level>
export const Level = Schema.Literals(["DEBUG", "INFO", "WARN", "ERROR"]).annotate({
identifier: "LogLevel",
description: "Log level",
})
export type Level = Schema.Schema.Type<typeof Level>
const levelPriority: Record<Level, number> = {
DEBUG: 0,
+206
View File
@@ -0,0 +1,206 @@
import { describe, expect } from "bun:test"
import { Effect, Exit, Stream } from "effect"
import { ChildProcess } from "effect/unstable/process"
import { AppProcess } from "@opencode-ai/core/process"
import { testEffect } from "../lib/effect"
const it = testEffect(AppProcess.defaultLayer)
const NODE = process.execPath
const cmd = (...args: string[]) => ChildProcess.make(NODE, args)
describe("AppProcess", () => {
describe("run", () => {
it.effect(
"captures stdout and exit code zero",
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const result = yield* svc.run(cmd("-e", "process.stdout.write('hi\\n')"))
expect(result.exitCode).toBe(0)
expect(result.stdout.toString("utf8")).toBe("hi\n")
expect(result.truncated).toBe(false)
}),
)
it.effect(
"non-zero exit returns RunResult; caller can require success",
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const result = yield* svc.run(cmd("-e", "process.exit(1)"))
expect(result.exitCode).toBe(1)
}),
)
it.effect(
"requireSuccess fails on non-zero exit",
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const exit = yield* Effect.exit(
svc.run(cmd("-e", "process.exit(1)")).pipe(Effect.flatMap(AppProcess.requireSuccess)),
)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) {
const reason = exit.cause.reasons[0]
if (reason && reason._tag === "Fail") {
expect(reason.error).toBeInstanceOf(AppProcess.AppProcessError)
expect((reason.error as AppProcess.AppProcessError).exitCode).toBe(1)
} else {
throw new Error("expected fail reason")
}
}
}),
)
it.effect(
"requireSuccess succeeds on exit 0",
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const result = yield* svc.run(cmd("-e", "process.exit(0)")).pipe(Effect.flatMap(AppProcess.requireSuccess))
expect(result.exitCode).toBe(0)
}),
)
it.effect(
"requireExitIn allowlists multiple exit codes",
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const requireZeroOrOne = AppProcess.requireExitIn([0, 1])
const okZero = yield* svc.run(cmd("-e", "process.exit(0)")).pipe(Effect.flatMap(requireZeroOrOne))
expect(okZero.exitCode).toBe(0)
const okOne = yield* svc.run(cmd("-e", "process.exit(1)")).pipe(Effect.flatMap(requireZeroOrOne))
expect(okOne.exitCode).toBe(1)
const exit = yield* Effect.exit(svc.run(cmd("-e", "process.exit(2)")).pipe(Effect.flatMap(requireZeroOrOne)))
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) {
const reason = exit.cause.reasons[0]
if (reason && reason._tag === "Fail") {
expect(reason.error).toBeInstanceOf(AppProcess.AppProcessError)
expect((reason.error as AppProcess.AppProcessError).exitCode).toBe(2)
}
}
}),
)
it.effect(
"truncates output when maxOutputBytes is set",
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const result = yield* svc.run(cmd("-e", "process.stdout.write('0123456789')"), { maxOutputBytes: 5 })
expect(result.exitCode).toBe(0)
expect(result.truncated).toBe(true)
expect(result.stdout.length).toBe(5)
expect(result.stdout.toString("utf8")).toBe("01234")
}),
)
it.effect(
"result includes command description",
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const result = yield* svc.run(cmd("-e", "process.stdout.write('hi')"))
expect(result.command).toBe(`${NODE} -e process.stdout.write('hi')`)
}),
)
})
describe("inherited platform methods", () => {
it.effect(
"string returns stdout as string",
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const out = yield* svc.string(cmd("-e", "process.stdout.write('hi\\n')"))
expect(out).toBe("hi\n")
}),
)
it.effect(
"lines returns the platform's array of lines",
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const out = yield* svc.lines(cmd("-e", "process.stdout.write('a\\nb\\n')"))
expect(Array.from(out)).toEqual(["a", "b"])
}),
)
})
describe("runStream", () => {
it.live(
"emits lines incrementally and ends cleanly on exit 0",
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const result = yield* svc
.runStream(cmd("-e", "console.log('one'); console.log('two'); console.log('three')"))
.pipe(Stream.runCollect)
expect(Array.from(result)).toEqual(["one", "two", "three"])
}),
)
it.live(
"fails with AppProcessError when exit not in okExitCodes",
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const exit = yield* Effect.exit(
svc.runStream(cmd("-e", "console.log('a'); process.exit(2)"), { okExitCodes: [0] }).pipe(Stream.runCollect),
)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) {
const reason = exit.cause.reasons[0]
if (reason && reason._tag === "Fail") {
expect(reason.error).toBeInstanceOf(AppProcess.AppProcessError)
}
}
}),
)
it.live(
"okExitCodes allowlist treats non-zero as success",
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const result = yield* svc
.runStream(cmd("-e", "console.log('only'); process.exit(1)"), { okExitCodes: [0, 1] })
.pipe(Stream.runCollect)
expect(Array.from(result)).toEqual(["only"])
}),
)
it.live(
"without okExitCodes, never fails on exit code",
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const result = yield* svc.runStream(cmd("-e", "console.log('only'); process.exit(7)")).pipe(Stream.runCollect)
expect(Array.from(result)).toEqual(["only"])
}),
)
it.live(
"AbortSignal interrupts the stream",
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const controller = new AbortController()
setTimeout(() => controller.abort(), 50)
const exit = yield* Effect.exit(
svc
.runStream(cmd("-e", "setInterval(() => console.log('tick'), 100); setTimeout(() => {}, 60_000)"), {
signal: controller.signal,
})
.pipe(Stream.runCollect),
)
expect(Exit.isFailure(exit)).toBe(true)
}),
)
})
describe("spawn (inherited)", () => {
it.live(
"returns the platform ChildProcessHandle for advanced use",
Effect.scoped(
Effect.gen(function* () {
const svc = yield* AppProcess.Service
const handle = yield* svc.spawn(cmd("-e", "setInterval(() => {}, 1_000)"))
expect(yield* handle.isRunning).toBe(true)
yield* handle.kill()
}),
),
)
})
})
+4 -1
View File
@@ -1,9 +1,12 @@
import { Message, Model, Part, Session, SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import { fn } from "@opencode-ai/core/util/fn"
import { iife } from "@opencode-ai/core/util/iife"
import z from "zod"
import { Storage } from "./storage"
function fn<T extends z.ZodType, Result>(schema: T, cb: (input: z.infer<T>) => Result) {
return (input: z.infer<T>) => cb(schema.parse(input))
}
export namespace Share {
export const Info = z.object({
id: z.string(),
+1
View File
@@ -298,6 +298,7 @@ declare module "sst" {
"EnterpriseStorage": cloudflare.R2Bucket
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"Stat": cloudflare.Service
"ZenData": cloudflare.R2Bucket
"ZenDataNew": cloudflare.R2Bucket
}
+1
View File
@@ -298,6 +298,7 @@ declare module "sst" {
"EnterpriseStorage": cloudflare.R2Bucket
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"Stat": cloudflare.Service
"ZenData": cloudflare.R2Bucket
"ZenDataNew": cloudflare.R2Bucket
}
+2 -2
View File
@@ -78,7 +78,7 @@ const streamText = LLM.stream(request).pipe(
Stream.tap((event) =>
Effect.sync(() => {
if (event.type === "text-delta") process.stdout.write(`\ntext: ${event.text}`)
if (event.type === "request-finish") process.stdout.write(`\nfinish: ${event.reason}\n`)
if (event.type === "finish") process.stdout.write(`\nfinish: ${event.reason}\n`)
}),
),
Stream.runDrain,
@@ -185,7 +185,7 @@ const FakeProtocol = Protocol.make<FakeBody, string, string, void>({
event: Schema.String,
initial: () => undefined,
step: (_, frame) => Effect.succeed([undefined, [{ type: "text-delta", id: "text-0", text: frame }]] as const),
onHalt: () => [{ type: "request-finish", reason: "stop" }],
onHalt: () => [{ type: "finish", reason: "stop" }],
},
})
+1
View File
@@ -17,6 +17,7 @@ export type {
ExecutableTools,
Tool as ToolShape,
ToolExecute,
ToolExecuteContext,
Tools,
ToolSchema,
} from "./tool"
@@ -380,7 +380,7 @@ type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
const NO_EVENTS: StepResult["1"] = []
// `response.completed` / `response.incomplete` are clean finishes that emit a
// `request-finish` event; `response.failed` is a hard failure that emits a
// `finish` event; `response.failed` is a hard failure that emits a
// `provider-error`. All three end the stream — kept in one set so `step` and
// the protocol's `terminal` predicate stay in sync.
const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"])
@@ -80,7 +80,7 @@ export const finish = (
usage: input.usage,
providerMetadata: input.providerMetadata,
}),
LLMEvent.requestFinish(input),
LLMEvent.finish(input),
)
return { ...stepped, stepStarted: false }
}
+17 -22
View File
@@ -1,5 +1,5 @@
import { Schema } from "effect"
import { ContentBlockID, FinishReason, ProtocolID, ProviderMetadata, ResponseID, RouteID, ToolCallID } from "./ids"
import { ContentBlockID, FinishReason, ProtocolID, ProviderMetadata, RouteID, ToolCallID } from "./ids"
import { ModelRef } from "./options"
import { ToolResultValue } from "./messages"
@@ -66,14 +66,13 @@ export class Usage extends Schema.Class<Usage>("LLM.Usage")({
get visibleOutputTokens() {
return Math.max(0, (this.outputTokens ?? 0) - (this.reasoningTokens ?? 0))
}
static from(input: UsageInput) {
return input instanceof Usage ? input : new Usage(input)
}
}
export const RequestStart = Schema.Struct({
type: Schema.tag("request-start"),
id: ResponseID,
model: ModelRef,
}).annotate({ identifier: "LLM.Event.RequestStart" })
export type RequestStart = Schema.Schema.Type<typeof RequestStart>
export type UsageInput = Usage | ConstructorParameters<typeof Usage>[0]
export const StepStart = Schema.Struct({
type: Schema.tag("step-start"),
@@ -185,13 +184,13 @@ export const StepFinish = Schema.Struct({
}).annotate({ identifier: "LLM.Event.StepFinish" })
export type StepFinish = Schema.Schema.Type<typeof StepFinish>
export const RequestFinish = Schema.Struct({
type: Schema.tag("request-finish"),
export const Finish = Schema.Struct({
type: Schema.tag("finish"),
reason: FinishReason,
usage: Schema.optional(Usage),
providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.RequestFinish" })
export type RequestFinish = Schema.Schema.Type<typeof RequestFinish>
}).annotate({ identifier: "LLM.Event.Finish" })
export type Finish = Schema.Schema.Type<typeof Finish>
export const ProviderErrorEvent = Schema.Struct({
type: Schema.tag("provider-error"),
@@ -202,7 +201,6 @@ export const ProviderErrorEvent = Schema.Struct({
export type ProviderErrorEvent = Schema.Schema.Type<typeof ProviderErrorEvent>
const llmEventTagged = Schema.Union([
RequestStart,
StepStart,
TextStart,
TextDelta,
@@ -217,16 +215,15 @@ const llmEventTagged = Schema.Union([
ToolResult,
ToolError,
StepFinish,
RequestFinish,
Finish,
ProviderErrorEvent,
]).pipe(Schema.toTaggedUnion("type"))
type WithID<Event extends { readonly id: unknown }, ID> = Omit<Event, "type" | "id"> & { readonly id: ID | string }
type WithUsage<Event extends { readonly usage?: Usage }> = Omit<Event, "type" | "usage"> & {
readonly usage?: Usage | ConstructorParameters<typeof Usage>[0]
readonly usage?: UsageInput
}
const responseID = (value: ResponseID | string) => ResponseID.make(value)
const contentBlockID = (value: ContentBlockID | string) => ContentBlockID.make(value)
const toolCallID = (value: ToolCallID | string) => ToolCallID.make(value)
@@ -236,7 +233,6 @@ const toolCallID = (value: ToolCallID | string) => ToolCallID.make(value)
* `events.filter(LLMEvent.guards["tool-call"])`.
*/
export const LLMEvent = Object.assign(llmEventTagged, {
requestStart: (input: WithID<RequestStart, ResponseID>) => RequestStart.make({ ...input, id: responseID(input.id) }),
stepStart: StepStart.make,
textStart: (input: WithID<TextStart, ContentBlockID>) => TextStart.make({ ...input, id: contentBlockID(input.id) }),
textDelta: (input: WithID<TextDelta, ContentBlockID>) => TextDelta.make({ ...input, id: contentBlockID(input.id) }),
@@ -258,16 +254,15 @@ export const LLMEvent = Object.assign(llmEventTagged, {
stepFinish: (input: WithUsage<StepFinish>) =>
StepFinish.make({
...input,
usage: input.usage === undefined ? undefined : input.usage instanceof Usage ? input.usage : new Usage(input.usage),
usage: input.usage === undefined ? undefined : Usage.from(input.usage),
}),
requestFinish: (input: WithUsage<RequestFinish>) =>
RequestFinish.make({
finish: (input: WithUsage<Finish>) =>
Finish.make({
...input,
usage: input.usage === undefined ? undefined : input.usage instanceof Usage ? input.usage : new Usage(input.usage),
usage: input.usage === undefined ? undefined : Usage.from(input.usage),
}),
providerError: ProviderErrorEvent.make,
is: {
requestStart: llmEventTagged.guards["request-start"],
stepStart: llmEventTagged.guards["step-start"],
textStart: llmEventTagged.guards["text-start"],
textDelta: llmEventTagged.guards["text-delta"],
@@ -282,7 +277,7 @@ export const LLMEvent = Object.assign(llmEventTagged, {
toolResult: llmEventTagged.guards["tool-result"],
toolError: llmEventTagged.guards["tool-error"],
stepFinish: llmEventTagged.guards["step-finish"],
requestFinish: llmEventTagged.guards["request-finish"],
finish: llmEventTagged.guards.finish,
providerError: llmEventTagged.guards["provider-error"],
},
})
+79 -9
View File
@@ -12,6 +12,7 @@ import {
ToolFailure,
ToolResultPart,
type ToolResultValue,
Usage,
} from "./schema"
import { type AnyTool, type ExecutableTools, type Tools, toDefinitions } from "./tool"
@@ -72,19 +73,42 @@ export const stream = <T extends Tools>(options: StreamOptions<T>): Stream.Strea
tools: [...options.request.tools.filter((tool) => !runtimeToolNames.has(tool.name)), ...runtimeTools],
})
const loop = (request: LLMRequest, step: number): Stream.Stream<LLMEvent, LLMError> =>
const loop = (
request: LLMRequest,
step: number,
usage: Usage | undefined,
providerMetadata: ProviderMetadata | undefined,
): Stream.Stream<LLMEvent, LLMError> =>
Stream.unwrap(
Effect.gen(function* () {
const state: StepState = { assistantContent: [], toolCalls: [], finishReason: undefined }
const state: StepState = {
assistantContent: [],
toolCalls: [],
finishReason: undefined,
usage: undefined,
providerMetadata: undefined,
}
const modelStream = options
.stream(request)
.pipe(Stream.map((event) => indexStep(event, step)))
.pipe(Stream.tap((event) => Effect.sync(() => accumulate(state, event))))
.pipe(Stream.filter((event) => event.type !== "finish"))
const continuation = Stream.unwrap(
Effect.gen(function* () {
if (state.finishReason !== "tool-calls" || state.toolCalls.length === 0) return Stream.empty
if (options.toolExecution === "none") return Stream.empty
const totalUsage = addUsage(usage, state.usage)
const totalProviderMetadata = mergeProviderMetadata(providerMetadata, state.providerMetadata)
const finishStream = Stream.fromIterable([
LLMEvent.finish({
reason: state.finishReason ?? "unknown",
usage: totalUsage,
providerMetadata: totalProviderMetadata,
}),
])
if (state.finishReason !== "tool-calls" || state.toolCalls.length === 0) return finishStream
if (options.toolExecution === "none") return finishStream
const dispatched = yield* Effect.forEach(
state.toolCalls,
@@ -93,10 +117,14 @@ export const stream = <T extends Tools>(options: StreamOptions<T>): Stream.Strea
)
const resultStream = Stream.fromIterable(dispatched.flatMap(([call, result]) => emitEvents(call, result)))
if (!options.stopWhen) return resultStream
if (options.stopWhen({ step, request })) return resultStream
if (!options.stopWhen) return resultStream.pipe(Stream.concat(finishStream))
if (options.stopWhen({ step, request })) return resultStream.pipe(Stream.concat(finishStream))
return resultStream.pipe(Stream.concat(loop(followUpRequest(request, state, dispatched), step + 1)))
return resultStream.pipe(
Stream.concat(
loop(followUpRequest(request, state, dispatched), step + 1, totalUsage, totalProviderMetadata),
),
)
}),
)
@@ -104,13 +132,21 @@ export const stream = <T extends Tools>(options: StreamOptions<T>): Stream.Strea
}),
)
return loop(initialRequest, 0)
return loop(initialRequest, 0, undefined, undefined)
}
const indexStep = (event: LLMEvent, index: number): LLMEvent => {
if (event.type === "step-start") return LLMEvent.stepStart({ index })
if (event.type === "step-finish") return LLMEvent.stepFinish({ ...event, index })
return event
}
interface StepState {
assistantContent: ContentPart[]
toolCalls: ToolCallPart[]
finishReason: FinishReason | undefined
usage: Usage | undefined
providerMetadata: ProviderMetadata | undefined
}
const accumulate = (state: StepState, event: LLMEvent) => {
@@ -154,9 +190,43 @@ const accumulate = (state: StepState, event: LLMEvent) => {
)
return
}
if (event.type === "step-finish" || event.type === "request-finish") {
if (event.type === "step-finish") {
state.finishReason = event.reason === "stop" && state.toolCalls.length > 0 ? "tool-calls" : event.reason
state.usage = addUsage(state.usage, event.usage)
state.providerMetadata = mergeProviderMetadata(state.providerMetadata, event.providerMetadata)
return
}
if (event.type === "finish") {
state.finishReason ??= event.reason
state.usage ??= event.usage
state.providerMetadata = mergeProviderMetadata(state.providerMetadata, event.providerMetadata)
}
}
const addUsage = (left: Usage | undefined, right: Usage | undefined) => {
if (!left) return right
if (!right) return left
type UsageKey =
| "inputTokens"
| "outputTokens"
| "nonCachedInputTokens"
| "cacheReadInputTokens"
| "cacheWriteInputTokens"
| "reasoningTokens"
| "totalTokens"
const sum = (key: UsageKey) =>
left[key] === undefined && right[key] === undefined ? undefined : Number(left[key] ?? 0) + Number(right[key] ?? 0)
return new Usage({
inputTokens: sum("inputTokens"),
outputTokens: sum("outputTokens"),
nonCachedInputTokens: sum("nonCachedInputTokens"),
cacheReadInputTokens: sum("cacheReadInputTokens"),
cacheWriteInputTokens: sum("cacheWriteInputTokens"),
reasoningTokens: sum("reasoningTokens"),
totalTokens: sum("totalTokens"),
providerMetadata: mergeProviderMetadata(left.providerMetadata, right.providerMetadata),
})
}
const sameProviderMetadata = (left: ProviderMetadata | undefined, right: ProviderMetadata | undefined) =>
+8 -10
View File
@@ -1,5 +1,5 @@
import { Effect, JsonSchema, Schema } from "effect"
import type { ToolDefinition as ToolDefinitionClass } from "./schema"
import type { ToolCallPart, ToolDefinition as ToolDefinitionClass } from "./schema"
import { ToolDefinition, ToolFailure } from "./schema"
/**
@@ -8,10 +8,14 @@ import { ToolDefinition, ToolFailure } from "./schema"
* beyond pure data conversion belongs in the handler closure.
*/
export type ToolSchema<T> = Schema.Codec<T, any, never, never>
export interface ToolExecuteContext {
readonly id: ToolCallPart["id"]
readonly name: ToolCallPart["name"]
}
export type ToolExecute<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = (
params: Schema.Schema.Type<Parameters>,
context?: { readonly id: string; readonly name: string },
context?: ToolExecuteContext,
) => Effect.Effect<Schema.Schema.Type<Success>, ToolFailure>
/**
@@ -62,10 +66,7 @@ type TypedToolConfig = {
type DynamicToolConfig = {
readonly description: string
readonly jsonSchema: JsonSchema.JsonSchema
readonly execute?: (
params: unknown,
context?: { readonly id: string; readonly name: string },
) => Effect.Effect<unknown, ToolFailure>
readonly execute?: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure>
}
/**
@@ -114,10 +115,7 @@ export function make<Parameters extends ToolSchema<any>, Success extends ToolSch
export function make(config: {
readonly description: string
readonly jsonSchema: JsonSchema.JsonSchema
readonly execute: (
params: unknown,
context?: { readonly id: string; readonly name: string },
) => Effect.Effect<unknown, ToolFailure>
readonly execute: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure>
}): AnyExecutableTool
export function make(config: {
readonly description: string
+3 -3
View File
@@ -51,7 +51,7 @@ const request = LLM.request({
const raiseEvent = (event: FakeEvent): import("../src/schema").LLMEvent =>
event.type === "finish"
? { type: "request-finish", reason: event.reason }
? { type: "finish", reason: event.reason }
: { type: "text-delta", id: "text-0", text: event.text }
const fakeProtocol = Protocol.make<FakeBody, FakeEvent, FakeEvent, void>({
@@ -112,8 +112,8 @@ describe("llm route", () => {
const events = Array.from(yield* llm.stream(request).pipe(Stream.runCollect))
const response = yield* llm.generate(request)
expect(events.map((event) => event.type)).toEqual(["text-delta", "request-finish"])
expect(response.events.map((event) => event.type)).toEqual(["text-delta", "request-finish"])
expect(events.map((event) => event.type)).toEqual(["text-delta", "finish"])
expect(response.events.map((event) => event.type)).toEqual(["text-delta", "finish"])
}),
)
+1 -1
View File
@@ -127,7 +127,7 @@ describe("llm constructors", () => {
LLMResponse.text({
events: [
{ type: "text-delta", id: "text-0", text: "hi" },
{ type: "request-finish", reason: "stop" },
{ type: "finish", reason: "stop" },
],
}),
).toBe("hi")
@@ -124,7 +124,7 @@ describe("Anthropic Messages route", () => {
providerMetadata: { anthropic: { signature: "sig_1" } },
})
expect(response.events.at(-1)).toMatchObject({
type: "request-finish",
type: "finish",
reason: "stop",
providerMetadata: { anthropic: { stopSequence: "\n\nHuman:" } },
})
@@ -182,7 +182,7 @@ describe("Anthropic Messages route", () => {
},
{ type: "step-finish", index: 0, reason: "tool-calls", usage, providerMetadata: undefined },
{
type: "request-finish",
type: "finish",
reason: "tool-calls",
providerMetadata: undefined,
usage,
@@ -275,7 +275,7 @@ describe("Anthropic Messages route", () => {
providerMetadata: { anthropic: { blockType: "web_search_tool_result" } },
})
expect(response.text).toBe("Found it.")
expect(response.events.at(-1)).toMatchObject({ type: "request-finish", reason: "stop" })
expect(response.events.at(-1)).toMatchObject({ type: "finish", reason: "stop" })
}),
)
@@ -169,12 +169,12 @@ describe("Bedrock Converse route", () => {
const response = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body)))
expect(response.text).toBe("Hello!")
const finishes = response.events.filter((event) => event.type === "request-finish")
const finishes = response.events.filter((event) => event.type === "finish")
// Bedrock splits the finish across `messageStop` (carries reason) and
// `metadata` (carries usage). We consolidate them into a single
// terminal `request-finish` event with both.
// terminal `finish` event with both.
expect(finishes).toHaveLength(1)
expect(finishes[0]).toMatchObject({ type: "request-finish", reason: "stop" })
expect(finishes[0]).toMatchObject({ type: "finish", reason: "stop" })
expect(response.usage).toMatchObject({
inputTokens: 5,
outputTokens: 2,
@@ -213,7 +213,7 @@ describe("Bedrock Converse route", () => {
{ type: "tool-input-delta", id: "tool_1", name: "lookup", text: '{"query"' },
{ type: "tool-input-delta", id: "tool_1", name: "lookup", text: ':"weather"}' },
])
expect(response.events.at(-1)).toMatchObject({ type: "request-finish", reason: "tool-calls" })
expect(response.events.at(-1)).toMatchObject({ type: "finish", reason: "tool-calls" })
}),
)
+7 -7
View File
@@ -232,7 +232,7 @@ describe("Gemini route", () => {
{ type: "text-end", id: "text-0" },
{ type: "step-finish", index: 0, reason: "stop", usage, providerMetadata: undefined },
{
type: "request-finish",
type: "finish",
reason: "stop",
usage,
},
@@ -291,7 +291,7 @@ describe("Gemini route", () => {
},
{ type: "step-finish", index: 0, reason: "tool-calls", usage, providerMetadata: undefined },
{
type: "request-finish",
type: "finish",
reason: "tool-calls",
usage,
},
@@ -325,7 +325,7 @@ describe("Gemini route", () => {
{ type: "tool-call", id: "tool_0", name: "lookup", input: { query: "weather" } },
{ type: "tool-call", id: "tool_1", name: "lookup", input: { query: "news" } },
])
expect(response.events.at(-1)).toMatchObject({ type: "request-finish", reason: "tool-calls" })
expect(response.events.at(-1)).toMatchObject({ type: "finish", reason: "tool-calls" })
}),
)
@@ -344,10 +344,10 @@ describe("Gemini route", () => {
),
)
expect(length.events.map((event) => event.type)).toEqual(["step-start", "step-finish", "request-finish"])
expect(length.events.at(-1)).toMatchObject({ type: "request-finish", reason: "length" })
expect(filtered.events.map((event) => event.type)).toEqual(["step-start", "step-finish", "request-finish"])
expect(filtered.events.at(-1)).toMatchObject({ type: "request-finish", reason: "content-filter" })
expect(length.events.map((event) => event.type)).toEqual(["step-start", "step-finish", "finish"])
expect(length.events.at(-1)).toMatchObject({ type: "finish", reason: "length" })
expect(filtered.events.map((event) => event.type)).toEqual(["step-start", "step-finish", "finish"])
expect(filtered.events.at(-1)).toMatchObject({ type: "finish", reason: "content-filter" })
}),
)
@@ -249,7 +249,7 @@ describe("OpenAI Chat route", () => {
{ type: "text-end", id: "text-0" },
{ type: "step-finish", index: 0, reason: "stop", usage, providerMetadata: undefined },
{
type: "request-finish",
type: "finish",
reason: "stop",
usage,
},
@@ -288,7 +288,7 @@ describe("OpenAI Chat route", () => {
providerMetadata: undefined,
},
{ type: "step-finish", index: 0, reason: "tool-calls", usage: undefined, providerMetadata: undefined },
{ type: "request-finish", reason: "tool-calls", usage: undefined },
{ type: "finish", reason: "tool-calls", usage: undefined },
])
}),
)
@@ -231,7 +231,7 @@ describe("OpenAI-compatible Chat route", () => {
expect(response.text).toBe("Hello!")
expect(response.usage).toMatchObject({ inputTokens: 5, outputTokens: 2, totalTokens: 7 })
expect(response.events.at(-1)).toMatchObject({ type: "request-finish", reason: "stop" })
expect(response.events.at(-1)).toMatchObject({ type: "finish", reason: "stop" })
}),
)
})
@@ -366,7 +366,7 @@ describe("OpenAI Responses route", () => {
usage,
},
{
type: "request-finish",
type: "finish",
reason: "stop",
providerMetadata: { openai: { responseId: "resp_1", serviceTier: "default" } },
usage,
@@ -447,7 +447,7 @@ describe("OpenAI Responses route", () => {
},
{ type: "step-finish", index: 0, reason: "tool-calls", usage, providerMetadata: undefined },
{
type: "request-finish",
type: "finish",
reason: "tool-calls",
providerMetadata: undefined,
usage,
+9 -7
View File
@@ -120,8 +120,8 @@ export const runWeatherToolLoop = (request: LLMRequest) =>
export const expectFinish = (
events: ReadonlyArray<LLMEvent>,
reason: Extract<LLMEvent, { readonly type: "request-finish" }>["reason"],
) => expect(events.at(-1)).toMatchObject({ type: "request-finish", reason })
reason: Extract<LLMEvent, { readonly type: "finish" }>["reason"],
) => expect(events.at(-1)).toMatchObject({ type: "finish", reason })
export const expectWeatherToolCall = (response: LLMResponse) =>
expect(response.toolCalls).toMatchObject([
@@ -129,10 +129,12 @@ export const expectWeatherToolCall = (response: LLMResponse) =>
])
export const expectWeatherToolLoop = (events: ReadonlyArray<LLMEvent>) => {
const finishes = events.filter(LLMEvent.is.requestFinish)
expect(finishes).toHaveLength(2)
expect(finishes[0]?.reason).toBe("tool-calls")
expect(finishes.at(-1)?.reason).toBe("stop")
const finishes = events.filter(LLMEvent.is.finish)
expect(finishes).toHaveLength(1)
expect(finishes[0]?.reason).toBe("stop")
const stepFinishes = events.filter(LLMEvent.is.stepFinish)
expect(stepFinishes.map((event) => event.reason)).toEqual(["tool-calls", "stop"])
const toolCalls = events.filter(LLMEvent.is.toolCall)
expect(toolCalls).toHaveLength(1)
@@ -272,7 +274,7 @@ export const eventSummary = (events: ReadonlyArray<LLMEvent>) => {
summary.push({ type: "tool-error", name: event.name, message: event.message })
continue
}
if (event.type === "request-finish") {
if (event.type === "finish") {
summary.push({ type: "finish", reason: event.reason, usage: usageSummary(event.usage) })
}
}
+5
View File
@@ -44,6 +44,11 @@ describe("llm schema", () => {
expect(() => Schema.decodeUnknownSync(LLMEvent)({ type: "bogus" })).toThrow()
})
test("finish constructors accept usage input", () => {
expect(LLMEvent.stepFinish({ index: 0, reason: "stop", usage: { inputTokens: 1 } }).usage).toBeInstanceOf(Usage)
expect(LLMEvent.finish({ reason: "stop", usage: { outputTokens: 2 } }).usage).toBeInstanceOf(Usage)
})
test("content part tagged union exposes guards", () => {
expect(ContentPart.guards.text({ type: "text", text: "hi" })).toBe(true)
expect(ContentPart.guards.media({ type: "text", text: "hi" })).toBe(false)
+86 -6
View File
@@ -4,7 +4,8 @@ import { GenerationOptions, LLM, LLMEvent, LLMRequest, LLMResponse, ToolChoice }
import { LLMClient } from "../src/route"
import * as AnthropicMessages from "../src/protocols/anthropic-messages"
import * as OpenAIChat from "../src/protocols/openai-chat"
import { tool, ToolFailure } from "../src/tool"
import { tool, ToolFailure, type ToolExecuteContext } from "../src/tool"
import { ToolRuntime } from "../src/tool-runtime"
import { it } from "./lib/effect"
import * as TestToolRuntime from "./lib/tool-runtime"
import { dynamicResponse, scriptedResponses } from "./lib/http"
@@ -129,7 +130,7 @@ describe("LLMClient tools", () => {
name: "get_weather",
result: { type: "json", value: { temperature: 22, condition: "sunny" } },
})
expect(events.at(-1)?.type).toBe("request-finish")
expect(events.at(-1)?.type).toBe("finish")
expect(LLMResponse.text({ events })).toBe("It's sunny in Paris.")
}),
)
@@ -148,11 +149,40 @@ describe("LLMClient tools", () => {
),
)
expect(events.filter(LLMEvent.is.requestFinish)).toHaveLength(1)
expect(events.filter(LLMEvent.is.finish)).toHaveLength(1)
expect(events.find(LLMEvent.is.toolResult)).toMatchObject({ type: "tool-result", id: "call_1" })
}),
)
it.effect("passes tool call context to execute", () =>
Effect.gen(function* () {
let context: ToolExecuteContext | undefined
const contextual = tool({
description: "Capture tool context.",
parameters: Schema.Struct({ value: Schema.String }),
success: Schema.Struct({ ok: Schema.Boolean }),
execute: (_params, ctx) =>
Effect.sync(() => {
context = ctx
return { ok: true }
}),
})
const events = Array.from(
yield* TestToolRuntime.runTools({ request: baseRequest, tools: { contextual } }).pipe(
Stream.runCollect,
Effect.provide(
scriptedResponses([
sseEvents(toolCallChunk("call_ctx", "contextual", '{"value":"x"}'), finishChunk("tool_calls")),
]),
),
),
)
expect(events.some(LLMEvent.is.toolResult)).toBe(true)
expect(context).toEqual({ id: "call_ctx", name: "contextual" })
}),
)
it.effect("can expose tool schemas without executing tool calls", () =>
Effect.gen(function* () {
const layer = scriptedResponses([
@@ -319,7 +349,7 @@ describe("LLMClient tools", () => {
"text-delta",
"text-end",
"step-finish",
"request-finish",
"finish",
])
expect(LLMResponse.text({ events })).toBe("Done.")
}),
@@ -343,7 +373,57 @@ describe("LLMClient tools", () => {
),
)
expect(events.filter(LLMEvent.is.requestFinish)).toHaveLength(2)
expect(events.filter(LLMEvent.is.finish)).toHaveLength(1)
expect(events.filter(LLMEvent.is.stepStart).map((event) => event.index)).toEqual([0, 1])
expect(events.filter(LLMEvent.is.stepFinish).map((event) => event.index)).toEqual([0, 1])
}),
)
it.effect("emits one final finish with aggregate usage", () =>
Effect.gen(function* () {
let calls = 0
const events = Array.from(
yield* ToolRuntime.stream({
request: baseRequest,
tools: { get_weather },
stopWhen: ToolRuntime.stepCountIs(2),
stream: () =>
Stream.fromIterable<LLMEvent>(
calls++ === 0
? [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call_1", name: "get_weather", input: { city: "Paris" } }),
LLMEvent.stepFinish({
index: 0,
reason: "tool-calls",
usage: { inputTokens: 1, outputTokens: 2, totalTokens: 3 },
}),
LLMEvent.finish({
reason: "tool-calls",
usage: { inputTokens: 1, outputTokens: 2, totalTokens: 3 },
}),
]
: [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.textDelta({ id: "text_1", text: "Done." }),
LLMEvent.stepFinish({
index: 0,
reason: "stop",
usage: { inputTokens: 4, outputTokens: 5, totalTokens: 9 },
}),
LLMEvent.finish({ reason: "stop", usage: { inputTokens: 4, outputTokens: 5, totalTokens: 9 } }),
],
),
}).pipe(Stream.runCollect),
)
expect(events.filter(LLMEvent.is.stepFinish).map((event) => event.index)).toEqual([0, 1])
expect(events.filter(LLMEvent.is.finish)).toHaveLength(1)
expect(events.find(LLMEvent.is.finish)?.usage).toMatchObject({
inputTokens: 5,
outputTokens: 7,
totalTokens: 12,
})
}),
)
@@ -362,7 +442,7 @@ describe("LLMClient tools", () => {
}).pipe(Stream.runCollect, Effect.provide(layer)),
)
expect(events.filter(LLMEvent.is.requestFinish)).toHaveLength(1)
expect(events.filter(LLMEvent.is.finish)).toHaveLength(1)
expect(events.find(LLMEvent.is.toolResult)).toMatchObject({ type: "tool-result", id: "call_1" })
}),
)
+1 -2
View File
@@ -105,10 +105,10 @@
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
"@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@openrouter/ai-sdk-provider": "2.8.1",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "2.6.1",
@@ -130,7 +130,6 @@
"bonjour-service": "1.3.0",
"bun-pty": "0.4.8",
"chokidar": "4.0.3",
"cli-sound": "1.1.3",
"clipboardy": "4.0.0",
"cross-spawn": "catalog:",
"decimal.js": "10.5.0",
+34
View File
@@ -29,6 +29,16 @@ Example:
"plugin": ["@acme/opencode-plugin@1.2.3", ["./plugins/demo.tsx", { "label": "demo" }]],
"plugin_enabled": {
"acme.demo": false
},
"attention": {
"enabled": true,
"notifications": true,
"sound": true,
"volume": 0.4,
"sound_pack": "opencode.default",
"sounds": {
"error": "/Users/me/sounds/error.mp3"
}
}
}
```
@@ -45,6 +55,11 @@ Example:
- Internal plugins can declare `enabled: false` to be registered but inactive by default; `plugin_enabled` and runtime KV can still enable them by id.
- `plugin_enabled` is merged across config layers.
- Runtime enable/disable state is also stored in KV under `plugin_enabled`; that KV state overrides config on startup.
- `attention.enabled` defaults to `false`; when `false`, it disables all `api.attention.notify(...)` delivery.
- `attention.notifications` and `attention.sound` independently control terminal-mediated desktop notifications and built-in sounds.
- `attention.volume` sets the default built-in sound volume from `0` to `1`.
- `attention.sound_pack` selects the initial semantic sound pack. Persisted runtime selection in KV can override it.
- `attention.sounds` overrides individual semantic sound slots such as `error`, `done`, or `subagent_done`.
- `leader_timeout` is a top-level TUI setting.
- `keybinds` is a flat object keyed by command id; values are key binding values (`false`, `"none"`, a key string/object, a binding object, or an array of key strings/objects/binding objects).
- `keybinds.leader` sets the key used by `<leader>` shortcuts.
@@ -212,6 +227,7 @@ That is what makes local config-scoped plugins able to import `@opencode-ai/plug
Top-level API groups exposed to `tui(api, options, meta)`:
- `api.app.version`
- `api.attention.notify(input)`
- `api.keys.formatSequence(parts)`, `formatBindings(bindings)`
- `api.keymap`
- `api.route.register(routes)` / `api.route.navigate(name, params?)` / `api.route.current`
@@ -246,6 +262,24 @@ Top-level API groups exposed to `tui(api, options, meta)`:
- `formatBindings(bindings)` formats binding lists and returns `undefined` when there is nothing to show.
- For generic config-to-bindings helpers, import `createBindingLookup` from `@opencode-ai/plugin/tui`.
### Attention
- `api.attention.notify({ title?, message, notification?, sound? })` requests user attention while keeping terminal focus, notifications, and audio owned by the host.
- `message` is required; `title` defaults to `"opencode"`; `notification` defaults to enabled with `when: "blurred"`; `sound` defaults to enabled with `when: "always"`.
- `when: "always"` requests delivery regardless of terminal focus state.
- `when: "focused"` only requests delivery after the terminal is known focused; `when: "blurred"` only requests delivery after the terminal is known blurred.
- Example: `notification: { when: "blurred" }, sound: { name: "question", when: "always" }` plays sound while focused but only triggers system notifications when blurred.
- Semantic sound names are `"default"`, `"question"`, `"permission"`, `"error"`, `"done"`, and `"subagent_done"`.
- `sound: true` plays the `"default"` sound; `sound: { name: "question" }` plays a named semantic sound.
- `sound: { volume }` overrides volume for that call; `sound: false` disables sound for that call; `notification: false` disables system notification for that call.
- `api.attention.soundboard.registerPack({ id, name?, sounds })` registers a sound pack and returns a disposer. Relative paths resolve from the plugin root and are cleaned up on plugin deactivation.
- `api.attention.soundboard.activate(id, { persist })` selects the active pack. `persist: true` writes the selected pack id to TUI KV state, not `tui.json`.
- `api.attention.soundboard.current()` and `list()` expose the active/registered packs for plugin UX.
- Config `attention.sounds` overrides active-pack sounds by slot. Failed loads fall back to the active pack and then `opencode.default`.
- The host strips ANSI/control characters and collapses newlines before sending text to the terminal notification API.
- Terminal and OS settings decide whether a requested notification is visibly displayed.
- Prefer privacy-safe messages such as `"A question needs your input"`; avoid full commands, paths, prompts, errors, secrets, or file contents unless the plugin intentionally exposes them.
### Routes
- Reserved route names: `home` and `session`.
@@ -0,0 +1,13 @@
# TUI Notifications Default
Problem:
- v1 defaults `attention.enabled` to `false`
- users can opt in with `attention.enabled = true`
- v2 should make core TUI notifications a default behavior
## v2 Target
Flip `attention.enabled` to `true` by default in v2.
Keep `attention.enabled = false` as the explicit opt-out.
+4 -3
View File
@@ -1094,8 +1094,8 @@ export class Agent implements ACPAgent {
const currentModeId = await (async () => {
if (!availableModes.length) return undefined
const defaultAgentName = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent()))
const resolvedModeId = availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
const defaultAgent = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultInfo()))
const resolvedModeId = availableModes.find((mode) => mode.name === defaultAgent.name)?.id ?? availableModes[0].id
this.sessionManager.setMode(sessionId, resolvedModeId)
return resolvedModeId
})()
@@ -1328,7 +1328,8 @@ export class Agent implements ACPAgent {
if (!current) {
this.sessionManager.setModel(session.id, model)
}
const agent = session.modeId ?? (await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent())))
const agent =
session.modeId ?? (await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultInfo()))).name
const parts: Array<
| { type: "text"; text: string; synthetic?: boolean; ignored?: boolean }
+17 -4
View File
@@ -1,4 +1,5 @@
import { Config } from "@/config/config"
import { ConfigPermission } from "@/config/permission"
import { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "../provider/schema"
import { generateObject, streamObject, type ModelMessage } from "ai"
@@ -57,6 +58,7 @@ const GeneratedAgent = Schema.Struct({
export interface Interface {
readonly get: (agent: string) => Effect.Effect<Info>
readonly list: () => Effect.Effect<Info[]>
readonly defaultInfo: () => Effect.Effect<Info>
readonly defaultAgent: () => Effect.Effect<string>
readonly generate: (input: {
description: string
@@ -116,7 +118,10 @@ export const layer = Layer.effect(
},
})
const user = Permission.fromConfig(cfg.permission ?? {})
// Convert permission layers to rulesets and merge them
// Each layer's rules come after the previous, so later configs override earlier ones
const layers = ConfigPermission.toLayers(cfg.permission)
const user = Permission.merge(...layers.map((p) => Permission.fromConfig(p)))
const agents: Record<string, Info> = {
build: {
@@ -333,23 +338,28 @@ export const layer = Layer.effect(
)
})
const defaultAgent = Effect.fnUntraced(function* () {
const defaultInfo = Effect.fnUntraced(function* () {
const c = yield* config.get()
if (c.default_agent) {
const agent = agents[c.default_agent]
if (!agent) throw new Error(`default agent "${c.default_agent}" not found`)
if (agent.mode === "subagent") throw new Error(`default agent "${c.default_agent}" is a subagent`)
if (agent.hidden === true) throw new Error(`default agent "${c.default_agent}" is hidden`)
return agent.name
return agent
}
const visible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true)
if (!visible) throw new Error("no primary visible agent found")
return visible.name
return visible
})
const defaultAgent = Effect.fnUntraced(function* () {
return (yield* defaultInfo()).name
})
return {
get,
list,
defaultInfo,
defaultAgent,
} satisfies State
}),
@@ -362,6 +372,9 @@ export const layer = Layer.effect(
list: Effect.fn("Agent.list")(function* () {
return yield* InstanceState.useEffect(state, (s) => s.list())
}),
defaultInfo: Effect.fn("Agent.defaultInfo")(function* () {
return yield* InstanceState.useEffect(state, (s) => s.defaultInfo())
}),
defaultAgent: Effect.fn("Agent.defaultAgent")(function* () {
return yield* InstanceState.useEffect(state, (s) => s.defaultAgent())
}),
@@ -5,10 +5,10 @@ import type { Agent } from "./agent"
* Build the `permission` ruleset for a subagent's session when it's spawned
* via the task tool. Combines:
*
* 1. The parent **agent's** deny rules — Plan Mode and other agent-level
* restrictions live on the agent ruleset, not on the session, so a
* 1. The parent **agent's** edit-class deny rules — Plan Mode's file-edit
* restriction lives on the agent ruleset, not on the session, so a
* subagent that only inherited the parent SESSION's permission would
* silently bypass them. (#26514)
* silently bypass it. (#26514)
* 2. The parent **session's** deny rules and external_directory rules —
* same forwarding the original code already did.
* 3. Default `todowrite` and `task` denies if the subagent's own ruleset
@@ -21,7 +21,8 @@ export function deriveSubagentSessionPermission(input: {
}): Permission.Ruleset {
const canTask = input.subagent.permission.some((rule) => rule.permission === "task")
const canTodo = input.subagent.permission.some((rule) => rule.permission === "todowrite")
const parentAgentDenies = input.parentAgent?.permission.filter((rule) => rule.action === "deny") ?? []
const parentAgentDenies =
input.parentAgent?.permission.filter((rule) => rule.action === "deny" && rule.permission === "edit") ?? []
return [
...parentAgentDenies,
...input.parentSessionPermission.filter(
+5
View File
@@ -3,6 +3,11 @@ declare module "*.wav" {
export default file
}
declare module "*.mp3" {
const file: string
export default file
}
declare module "*.wasm" {
const file: string
export default file
+4 -1
View File
@@ -124,6 +124,7 @@ const handlePluginAuth = Effect.fn("Cli.providers.pluginAuth")(function* (
yield* put(saveProvider, {
type: "api",
key: result.key,
...(result.metadata ? { metadata: result.metadata } : {}),
})
}
yield* spinner.stop("Login successful")
@@ -156,6 +157,7 @@ const handlePluginAuth = Effect.fn("Cli.providers.pluginAuth")(function* (
yield* put(saveProvider, {
type: "api",
key: result.key,
...(result.metadata ? { metadata: result.metadata } : {}),
})
}
yield* Prompt.log.success("Login successful")
@@ -191,10 +193,11 @@ const handlePluginAuth = Effect.fn("Cli.providers.pluginAuth")(function* (
}
if (result.type === "success") {
const saveProvider = result.provider ?? provider
const merged = { ...(metadata.metadata ?? {}), ...(result.metadata ?? {}) }
yield* put(saveProvider, {
type: "api",
key: result.key ?? apiKey,
...metadata,
...(Object.keys(merged).length ? { metadata: merged } : {}),
})
yield* Prompt.log.success("Login successful")
}
+10 -2
View File
@@ -2,6 +2,7 @@ import { render, TimeToFirstDraw, useRenderer, useTerminalDimensions } from "@op
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
import * as Clipboard from "@tui/util/clipboard"
import * as Selection from "@tui/util/selection"
import * as TuiAudio from "@tui/util/audio"
import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
import { RouteProvider, useRoute } from "@tui/context/route"
import {
@@ -63,6 +64,7 @@ import { TuiConfig } from "@/cli/cmd/tui/config/tui"
import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
import { createTuiApi } from "@/cli/cmd/tui/plugin/api"
import type { RouteMap } from "@/cli/cmd/tui/plugin/api"
import { createTuiAttention } from "@/cli/cmd/tui/attention"
import { FormatError, FormatUnknownError } from "@/cli/error"
import { CommandPaletteProvider, useCommandPalette } from "./context/command-palette"
import { OpencodeKeymapProvider, registerOpencodeKeymap, useBindings, useOpencodeKeymap } from "./keymap"
@@ -176,10 +178,10 @@ export function tui(input: {
unguard?.()
resolve()
}
const onBeforeExit = async () => {
offKeymap()
await TuiPluginRuntime.dispose()
TuiAudio.dispose()
}
const renderer = await createCliRenderer(rendererConfig(input.config))
@@ -283,6 +285,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
routeRev()
return routes.get(name)?.at(-1)?.render
}
const attention = createTuiAttention({ renderer, config: tuiConfig, kv })
const api = createTuiApi({
tuiConfig,
@@ -298,11 +301,13 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
theme: themeState,
toast,
renderer,
attention,
})
const [ready, setReady] = createSignal(false)
TuiPluginRuntime.init({
api,
config: tuiConfig,
dispose: () => attention.dispose(),
})
.catch((error) => {
console.error("Failed to load TUI plugins", error)
@@ -320,7 +325,10 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
},
{ priority: 1 },
)
onCleanup(offSelectionKeys)
onCleanup(() => {
offSelectionKeys()
attention.dispose()
})
// Wire up console copy-to-clipboard via opentui's onCopySelection callback
renderer.console.onCopySelection = async (text: string) => {
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,262 @@
import type {
TuiAttention,
TuiAttentionNotifyInput,
TuiAttentionNotifyResult,
TuiAttentionNotifySkipReason,
TuiAttentionWhen,
TuiKV,
TuiAttentionSoundName,
TuiAttentionSoundPack,
TuiAttentionSoundPackInfo,
} from "@opencode-ai/plugin/tui"
import stripAnsi from "strip-ansi"
import type { TuiConfig } from "./config/tui"
import { isAttentionSoundName } from "./config/tui-schema"
import * as TuiAudio from "@tui/util/audio"
import defaultSoundPath from "@opencode-ai/ui/audio/bip-bop-01.mp3" with { type: "file" }
import questionSoundPath from "@opencode-ai/ui/audio/bip-bop-03.mp3" with { type: "file" }
import permissionSoundPath from "@opencode-ai/ui/audio/staplebops-06.mp3" with { type: "file" }
import errorSoundPath from "@opencode-ai/ui/audio/nope-03.mp3" with { type: "file" }
import doneSoundPath from "@opencode-ai/ui/audio/bip-bop-01.mp3" with { type: "file" }
import subagentDoneSoundPath from "@opencode-ai/ui/audio/yup-01.mp3" with { type: "file" }
import * as Log from "@opencode-ai/core/util/log"
type FocusState = "unknown" | "focused" | "blurred"
type AttentionRenderer = {
readonly isDestroyed: boolean
on(event: "focus" | "blur", listener: () => void): unknown
off(event: "focus" | "blur", listener: () => void): unknown
triggerNotification(message: string, title?: string): boolean
}
type RegisteredSoundPack = TuiAttentionSoundPack & {
builtin: boolean
}
type TuiAttentionHost = TuiAttention & {
dispose(): void
}
const log = Log.create({ service: "tui.attention" })
const DEFAULT_TITLE = "opencode"
const DEFAULT_PACK_ID = "opencode.default"
const KV_SOUND_PACK = "attention_sound_pack"
const TITLE_LIMIT = 80
const MESSAGE_LIMIT = 240
const BUILTIN_PACK: RegisteredSoundPack = {
id: DEFAULT_PACK_ID,
name: "OpenCode Default",
builtin: true,
sounds: {
default: defaultSoundPath,
question: questionSoundPath,
permission: permissionSoundPath,
error: errorSoundPath,
done: doneSoundPath,
subagent_done: subagentDoneSoundPath,
},
}
function skipped(reason: TuiAttentionNotifySkipReason): TuiAttentionNotifyResult {
return {
ok: false,
notification: false,
sound: false,
skipped: reason,
}
}
function normalizeText(input: string | undefined, fallback: string, limit: number) {
const text = stripAnsi(input ?? "")
.replace(/[ \t]*[\r\n]+[ \t]*/g, " ")
.replace(/[\u0000-\u0009\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, "")
.trim()
const normalized = text.length ? text : fallback
return Array.from(normalized).slice(0, limit).join("")
}
function clampVolume(volume: number) {
if (!Number.isFinite(volume)) return 0
return Math.min(1, Math.max(0, volume))
}
function soundVolume(input: TuiAttentionNotifyInput, config: Pick<TuiConfig.Resolved, "attention">) {
if (!config.attention.sound) return
if (input.sound === false) return
if (input.sound === undefined) return clampVolume(config.attention.volume)
if (input.sound === true) return clampVolume(config.attention.volume)
return clampVolume(input.sound.volume ?? config.attention.volume)
}
function normalizePack(pack: TuiAttentionSoundPack): RegisteredSoundPack | undefined {
const id = pack.id.trim()
if (!id) return
return {
id,
name: pack.name?.trim() || undefined,
builtin: false,
sounds: Object.fromEntries(
Object.entries(pack.sounds).filter(
(item): item is [TuiAttentionSoundName, string] =>
isAttentionSoundName(item[0]) && typeof item[1] === "string" && item[1].trim().length > 0,
),
),
}
}
function focusSkip(when: TuiAttentionWhen, focus: FocusState) {
if (when === "always") return
if (focus === "unknown") return "focus_unknown"
if (when === "blurred" && focus === "focused") return "focused"
if (when === "focused" && focus === "blurred") return "blurred"
}
export function createTuiAttention(input: {
renderer: AttentionRenderer
config: Pick<TuiConfig.Resolved, "attention">
kv?: TuiKV
audio?: Pick<typeof TuiAudio, "loadSoundFile" | "play">
}): TuiAttentionHost {
let focus: FocusState = "unknown"
let disposed = false
let activePackID: string | undefined
const packs = new Map<string, RegisteredSoundPack>([[BUILTIN_PACK.id, BUILTIN_PACK]])
const audio = input.audio ?? TuiAudio
const onFocus = () => {
focus = "focused"
}
const onBlur = () => {
focus = "blurred"
}
input.renderer.on("focus", onFocus)
input.renderer.on("blur", onBlur)
function configuredPackID() {
const stored = input.kv?.get<string | undefined>(KV_SOUND_PACK, undefined)
return activePackID ?? stored ?? input.config.attention.sound_pack
}
function currentPack() {
return packs.get(configuredPackID()) ?? BUILTIN_PACK
}
function soundCandidates(name: TuiAttentionSoundName) {
return [input.config.attention.sounds[name], currentPack().sounds[name], BUILTIN_PACK.sounds[name]].filter(
(item, index, list): item is string => typeof item === "string" && list.indexOf(item) === index,
)
}
async function playSound(name: TuiAttentionSoundName, volume: number) {
try {
for (const file of soundCandidates(name)) {
const current = await audio.loadSoundFile(file).catch((error) => {
log.debug("failed to load attention sound", { file, error })
return null
})
if (disposed) return false
if (current == null) continue
if (audio.play(current, { volume }) != null) return true
}
return false
} catch (error) {
log.debug("failed to play attention sound", { error })
return false
}
}
return {
async notify(request) {
try {
if (!input.config.attention.enabled) return skipped("attention_disabled")
if (disposed || input.renderer.isDestroyed) return skipped("renderer_destroyed")
const message = normalizeText(request.message, "", MESSAGE_LIMIT)
if (!message) return skipped("empty_message")
const requestedNotification = typeof request.notification === "object" ? request.notification : undefined
const notificationSkip = focusSkip(requestedNotification?.when ?? "blurred", focus)
const notificationRequested = input.config.attention.notifications && request.notification !== false
const shouldNotify = notificationRequested && !notificationSkip
const notification = shouldNotify
? (() => {
try {
return input.renderer.triggerNotification(
message,
normalizeText(request.title, DEFAULT_TITLE, TITLE_LIMIT),
)
} catch (error) {
log.debug("failed to trigger attention notification", { error })
return false
}
})()
: false
const volume = soundVolume(request, input.config)
const requestedSound = typeof request.sound === "object" ? request.sound : undefined
const soundSkip = volume === undefined ? undefined : focusSkip(requestedSound?.when ?? "always", focus)
const soundName =
requestedSound?.name && isAttentionSoundName(requestedSound.name) ? requestedSound.name : "default"
const sound = volume === undefined || soundSkip ? false : await playSound(soundName, volume)
if (!notification && !sound) {
if (notificationRequested && notificationSkip) return skipped(notificationSkip)
if (soundSkip) return skipped(soundSkip)
}
return {
ok: notification || sound,
notification,
sound,
}
} catch (error) {
log.debug("failed to handle attention notification", { error })
return {
ok: false,
notification: false,
sound: false,
}
}
},
soundboard: {
registerPack(pack) {
const next = normalizePack(pack)
if (!next) return () => {}
packs.set(next.id, next)
let disposed = false
return () => {
if (disposed) return
disposed = true
if (packs.get(next.id) === next) packs.delete(next.id)
}
},
activate(id, options) {
const pack = packs.get(id)
if (!pack) return false
activePackID = pack.id
if (options?.persist) input.kv?.set(KV_SOUND_PACK, pack.id)
return true
},
current() {
return currentPack().id
},
list(): TuiAttentionSoundPackInfo[] {
const current = currentPack().id
return Array.from(packs.values()).map((pack) => ({
id: pack.id,
name: pack.name,
active: pack.id === current,
builtin: pack.builtin,
}))
},
},
dispose() {
if (disposed) return
disposed = true
input.renderer.off("focus", onFocus)
input.renderer.off("blur", onBlur)
},
}
}
@@ -2,7 +2,6 @@ import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@o
import { useRenderer } from "@opentui/solid"
import { For, createMemo, createSignal, onCleanup, onMount, type JSX } from "solid-js"
import { useTheme, tint } from "@tui/context/theme"
import * as Sound from "@tui/util/sound"
import { go, logo } from "@/cli/logo"
export type LogoShape = {
@@ -563,7 +562,6 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } =
const [now, setNow] = createSignal(0)
let box: BoxRenderable | undefined
let timer: ReturnType<typeof setInterval> | undefined
let hum = false
const stop = () => {
if (!timer) return
@@ -575,10 +573,6 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } =
const t = performance.now()
setNow(t)
const item = hold()
if (item && !hum && t - item.at >= HOLD) {
hum = true
Sound.start()
}
if (item && t - item.at >= CHARGE) {
burst(item.x, item.y)
}
@@ -605,8 +599,6 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } =
onCleanup(() => {
stop()
hum = false
Sound.dispose()
})
onMount(() => {
@@ -626,14 +618,12 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } =
setNow(t)
if (!last) setRelease(undefined)
setHold({ x, y, at: t, glyph: select(x, y, ctx) })
hum = false
start()
}
const burst = (x: number, y: number) => {
const item = hold()
if (!item) return
hum = false
const t = performance.now()
const age = t - item.at
const rise = ramp(age, HOLD, CHARGE)
@@ -655,7 +645,6 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } =
])
setNow(t)
start()
Sound.pulse(lerp(0.8, 1, level))
}
const frame = createMemo(() => {
@@ -1,12 +1,47 @@
import { ConfigPlugin } from "@/config/plugin"
import { TuiKeybind } from "./keybind"
import { Schema } from "effect"
import { isRecord } from "@/util/record"
import { Filesystem } from "@/util/filesystem"
import { TuiAttentionSoundNames, type TuiAttentionSoundName } from "@opencode-ai/plugin/tui"
export type TuiAttentionSoundPaths = Partial<Record<TuiAttentionSoundName, string>>
export function isAttentionSoundName(value: string): value is TuiAttentionSoundName {
return TuiAttentionSoundNames.includes(value as TuiAttentionSoundName)
}
export function resolveAttentionSoundPaths(
root: string,
sounds: unknown,
options?: { trim?: boolean },
): TuiAttentionSoundPaths {
if (!isRecord(sounds)) return {}
return Object.fromEntries(
Object.entries(sounds).flatMap(([name, file]) => {
if (!isAttentionSoundName(name)) return []
if (typeof file !== "string") return []
const value = options?.trim ? file.trim() : file
if (!value) return []
return [[name, Filesystem.resolveFilePath(root, value)]]
}),
)
}
export const KeymapLeaderTimeoutDefault = 2000
const KeymapLeaderTimeout = Schema.Int.check(Schema.isGreaterThan(0)).annotate({
description: "Leader key timeout in milliseconds",
})
const TuiAttentionSounds = Schema.Struct({
default: Schema.optional(Schema.String),
question: Schema.optional(Schema.String),
permission: Schema.optional(Schema.String),
error: Schema.optional(Schema.String),
done: Schema.optional(Schema.String),
subagent_done: Schema.optional(Schema.String),
})
export const ScrollSpeed = Schema.Number.check(Schema.isGreaterThanOrEqualTo(0.001))
export const ScrollAcceleration = Schema.Struct({
@@ -17,6 +52,15 @@ export const DiffStyle = Schema.Literals(["auto", "stacked"]).annotate({
description: "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column",
})
export const Attention = Schema.Struct({
enabled: Schema.optional(Schema.Boolean),
notifications: Schema.optional(Schema.Boolean),
sound: Schema.optional(Schema.Boolean),
volume: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(1))),
sound_pack: Schema.optional(Schema.String),
sounds: Schema.optional(TuiAttentionSounds),
}).annotate({ description: "Attention notification and sound settings" })
export const TuiInfo = Schema.Struct({
$schema: Schema.optional(Schema.String),
theme: Schema.optional(Schema.String),
@@ -24,6 +68,7 @@ export const TuiInfo = Schema.Struct({
plugin: Schema.optional(Schema.Array(ConfigPlugin.Spec)),
plugin_enabled: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
leader_timeout: Schema.optional(KeymapLeaderTimeout),
attention: Schema.optional(Attention),
scroll_speed: Schema.optional(ScrollSpeed).annotate({
description: "TUI scroll speed",
}),
@@ -1,5 +1,6 @@
export * as TuiConfig from "./tui"
import path from "path"
import { createBindingLookup } from "@opentui/keymap/extras"
import { mergeDeep, unique } from "remeda"
import { Context, Effect, Fiber, Layer, Schema } from "effect"
@@ -7,7 +8,7 @@ import { ConfigParse } from "@/config/parse"
import { InvalidError } from "@/config/error"
import * as ConfigPaths from "@/config/paths"
import { migrateTuiConfig } from "./tui-migrate"
import { KeymapLeaderTimeoutDefault, TuiInfo } from "./tui-schema"
import { KeymapLeaderTimeoutDefault, resolveAttentionSoundPaths, TuiInfo } from "./tui-schema"
import { Flag } from "@opencode-ai/core/flag/flag"
import { isRecord } from "@/util/record"
import { Global } from "@opencode-ai/core/global"
@@ -22,6 +23,7 @@ import * as Log from "@opencode-ai/core/util/log"
import { ConfigVariable } from "@/config/variable"
import { Npm } from "@opencode-ai/core/npm"
import type { DeepMutable } from "@opencode-ai/core/schema"
import type { TuiAttentionSoundName } from "@opencode-ai/plugin/tui"
const log = Log.create({ service: "tui.config" })
@@ -33,7 +35,15 @@ type Acc = {
plugin_origins: ConfigPlugin.Origin[]
}
export type Resolved = Omit<Info, "keybinds" | "leader_timeout"> & {
export type Resolved = Omit<Info, "attention" | "keybinds" | "leader_timeout"> & {
attention: {
enabled: boolean
notifications: boolean
sound: boolean
volume: number
sound_pack: string
sounds: Partial<Record<TuiAttentionSoundName, string>>
}
keybinds: TuiKeybind.BindingLookupView
leader_timeout: number
// Internal resolved plugin list used by runtime loading.
@@ -101,7 +111,16 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
})
}
}
const validated = ConfigParse.schema(Info, normalized, configFilepath)
const parsed = ConfigParse.schema(Info, normalized, configFilepath)
const validated = parsed.attention?.sounds
? {
...parsed,
attention: {
...parsed.attention,
sounds: resolveAttentionSoundPaths(path.dirname(configFilepath), parsed.attention.sounds),
},
}
: parsed
return yield* resolvePlugins(validated, configFilepath)
}).pipe(
// catchCause (not tapErrorCause + orElseSucceed) because JSONC parsing and validation
@@ -197,6 +216,14 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
const parsedKeybinds = TuiKeybind.parse(keybinds)
const result: Resolved = {
...acc.result,
attention: {
enabled: acc.result.attention?.enabled ?? false,
notifications: acc.result.attention?.notifications ?? true,
sound: acc.result.attention?.sound ?? true,
volume: acc.result.attention?.volume ?? 0.4,
sound_pack: acc.result.attention?.sound_pack ?? "opencode.default",
sounds: acc.result.attention?.sounds ?? {},
},
keybinds: createBindingLookup(TuiKeybind.toBindingConfig(parsedKeybinds), {
commandMap: TuiKeybind.CommandMap,
bindingDefaults: TuiKeybind.bindingDefaults(),
@@ -1,11 +1,52 @@
import { createMemo, For } from "solid-js"
import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
import { createMemo, For, type Accessor } from "solid-js"
import { DEFAULT_THEMES, useTheme } from "@tui/context/theme"
import { Flag } from "@opencode-ai/core/flag/flag"
import { useCommandShortcut } from "../../keymap"
const themeCount = Object.keys(DEFAULT_THEMES).length
const themeTip = `Use {highlight}/themes{/highlight} or {highlight}Ctrl+X T{/highlight} to switch between ${themeCount} built-in themes`
type TipPart = { text: string; highlight: boolean }
type TipShortcut = Accessor<string>
type Shortcuts = {
agentCycle: TipShortcut
childFirst: TipShortcut
childNext: TipShortcut
childPrevious: TipShortcut
commandList: TipShortcut
editorOpen: TipShortcut
helpShow: TipShortcut
inputClear: TipShortcut
inputNewline: TipShortcut
inputPaste: TipShortcut
inputUndo: TipShortcut
leader: TipShortcut
messagesCopy: TipShortcut
messagesFirst: TipShortcut
messagesLast: TipShortcut
messagesPageDown: TipShortcut
messagesPageUp: TipShortcut
messagesToggleConceal: TipShortcut
modelCycleRecent: TipShortcut
modelList: TipShortcut
sessionCycleRecent: TipShortcut
sessionCycleRecentReverse: TipShortcut
sessionExport: TipShortcut
sessionInterrupt: TipShortcut
sessionList: TipShortcut
sessionNew: TipShortcut
sessionParent: TipShortcut
sessionPinToggle: TipShortcut
sessionQuickSwitch1: TipShortcut
sessionQuickSwitch9: TipShortcut
sessionSidebarToggle: TipShortcut
sessionTimeline: TipShortcut
sessionToggleRecent: TipShortcut
statusView: TipShortcut
terminalSuspend: TipShortcut
themeList: TipShortcut
}
type Tip = string | ((shortcuts: Shortcuts) => string | undefined)
function parse(tip: string): TipPart[] {
const parts: TipPart[] = []
@@ -33,17 +74,86 @@ function parse(tip: string): TipPart[] {
const NO_MODELS_TIP = "Run {highlight}/connect{/highlight} to add an AI provider and start coding"
export function Tips(props: { connected?: boolean }) {
function shortcutText(value: string) {
return `{highlight}${value}{/highlight}`
}
function commandText(command: string, shortcut: string) {
if (!shortcut) return shortcutText(command)
return `${shortcutText(command)} or ${shortcutText(shortcut)}`
}
function press(shortcut: string, text: string) {
if (!shortcut) return undefined
return `Press ${shortcutText(shortcut)} ${text}`
}
function configShortcut(api: TuiPluginApi, command: string): TipShortcut {
return () =>
api.tuiConfig.keybinds
.get(command)
.map((binding) => api.keys.formatSequence(Array.from(api.keymap.parseKeySequence(binding.key))))
.filter(Boolean)
.join(", ")
}
export function Tips(props: { api: TuiPluginApi; connected?: boolean }) {
const theme = useTheme().theme
const randomTip = TIPS[Math.floor(Math.random() * TIPS.length)]
const parts = createMemo(() => parse(props.connected === false ? NO_MODELS_TIP : randomTip))
const tipOffset = Math.random()
const shortcuts: Shortcuts = {
agentCycle: useCommandShortcut("agent.cycle"),
childFirst: configShortcut(props.api, "session.child.first"),
childNext: configShortcut(props.api, "session.child.next"),
childPrevious: configShortcut(props.api, "session.child.previous"),
commandList: useCommandShortcut("command.palette.show"),
editorOpen: useCommandShortcut("prompt.editor"),
helpShow: useCommandShortcut("help.show"),
inputClear: useCommandShortcut("prompt.clear"),
inputNewline: useCommandShortcut("input.newline"),
inputPaste: useCommandShortcut("prompt.paste"),
inputUndo: useCommandShortcut("input.undo"),
leader: configShortcut(props.api, "leader"),
messagesCopy: configShortcut(props.api, "messages.copy"),
messagesFirst: configShortcut(props.api, "session.first"),
messagesLast: configShortcut(props.api, "session.last"),
messagesPageDown: configShortcut(props.api, "session.page.down"),
messagesPageUp: configShortcut(props.api, "session.page.up"),
messagesToggleConceal: configShortcut(props.api, "session.toggle.conceal"),
modelCycleRecent: useCommandShortcut("model.cycle_recent"),
modelList: useCommandShortcut("model.list"),
sessionCycleRecent: useCommandShortcut("session.cycle_recent"),
sessionCycleRecentReverse: useCommandShortcut("session.cycle_recent_reverse"),
sessionExport: configShortcut(props.api, "session.export"),
sessionInterrupt: configShortcut(props.api, "session.interrupt"),
sessionList: useCommandShortcut("session.list"),
sessionNew: useCommandShortcut("session.new"),
sessionParent: configShortcut(props.api, "session.parent"),
sessionPinToggle: configShortcut(props.api, "session.pin.toggle"),
sessionQuickSwitch1: useCommandShortcut("session.quick_switch.1"),
sessionQuickSwitch9: useCommandShortcut("session.quick_switch.9"),
sessionSidebarToggle: configShortcut(props.api, "session.sidebar.toggle"),
sessionTimeline: configShortcut(props.api, "session.timeline"),
sessionToggleRecent: configShortcut(props.api, "session.toggle.recent"),
statusView: useCommandShortcut("opencode.status"),
terminalSuspend: useCommandShortcut("terminal.suspend"),
themeList: useCommandShortcut("theme.switch"),
}
const tip = createMemo(() => {
if (props.connected === false) return NO_MODELS_TIP
const tips = TIPS.flatMap((item) => {
const value = typeof item === "string" ? item : item(shortcuts)
return value ? [value] : []
})
return tips[Math.floor(tipOffset * tips.length)] ?? NO_MODELS_TIP
})
const parts = createMemo(() => parse(tip()))
return (
<box flexDirection="row" maxWidth="100%">
<text flexShrink={0} style={{ fg: theme.warning }}>
Tip{" "}
</text>
<text flexShrink={1}>
<text flexShrink={1} wrapMode="word">
<For each={parts()}>
{(part) => <span style={{ fg: part.highlight ? theme.text : theme.textMuted }}>{part.text}</span>}
</For>
@@ -52,46 +162,66 @@ export function Tips(props: { connected?: boolean }) {
)
}
const TIPS = [
const TIPS: Tip[] = [
"Type {highlight}@{/highlight} followed by a filename to fuzzy search and attach files",
"Start a message with {highlight}!{/highlight} to run shell commands directly (e.g., {highlight}!ls -la{/highlight})",
"Press {highlight}Tab{/highlight} to cycle between Build and Plan agents",
(shortcuts) => press(shortcuts.agentCycle(), "to cycle between Build and Plan agents"),
"Use {highlight}/undo{/highlight} to revert the last message and file changes",
"Use {highlight}/redo{/highlight} to restore previously undone messages and file changes",
"Run {highlight}/share{/highlight} to create a public link to your conversation at opencode.ai",
"Drag and drop images or PDFs into the terminal to add them as context",
"Press {highlight}Ctrl+V{/highlight} to paste images from your clipboard into the prompt",
"Press {highlight}Ctrl+X E{/highlight} or {highlight}/editor{/highlight} to compose messages in your external editor",
(shortcuts) => press(shortcuts.inputPaste(), "to paste images from your clipboard into the prompt"),
(shortcuts) => `Use ${commandText("/editor", shortcuts.editorOpen())} to compose messages in your external editor`,
"Run {highlight}/init{/highlight} to auto-generate project rules based on your codebase",
"Run {highlight}/models{/highlight} or {highlight}Ctrl+X M{/highlight} to see and switch between available AI models",
themeTip,
"Press {highlight}Ctrl+X N{/highlight} or {highlight}/new{/highlight} to start a fresh conversation session",
"Use {highlight}/sessions{/highlight} or {highlight}Ctrl+X L{/highlight} to list and continue previous conversations",
(shortcuts) => `Use ${commandText("/models", shortcuts.modelList())} to see and switch between available AI models`,
(shortcuts) => `Use ${commandText("/themes", shortcuts.themeList())} to switch between ${themeCount} built-in themes`,
(shortcuts) => `Use ${commandText("/new", shortcuts.sessionNew())} to start a fresh conversation session`,
(shortcuts) => `Use ${commandText("/sessions", shortcuts.sessionList())} to list and continue previous conversations`,
...(Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
? [
"Press {highlight}Ctrl+F{/highlight} in the session list to pin a session so it stays at the top",
"Pinned and recent sessions are bound to {highlight}Ctrl+X 1{/highlight} through {highlight}Ctrl+X 9{/highlight} for one-press switching",
"Press {highlight}Ctrl+X ]{/highlight} / {highlight}Ctrl+X [{/highlight} to cycle through recently visited sessions",
"Press {highlight}Ctrl+H{/highlight} in the session list to show or hide a session in the Recent group",
]
? ([
(shortcuts) =>
press(shortcuts.sessionPinToggle(), "in the session list to pin a session so it stays at the top"),
(shortcuts) =>
shortcuts.sessionQuickSwitch1() && shortcuts.sessionQuickSwitch9()
? `Pinned and recent sessions are bound to ${shortcutText(shortcuts.sessionQuickSwitch1())} through ${shortcutText(shortcuts.sessionQuickSwitch9())} for one-press switching`
: undefined,
(shortcuts) =>
shortcuts.sessionCycleRecent() && shortcuts.sessionCycleRecentReverse()
? `Press ${shortcutText(shortcuts.sessionCycleRecent())} / ${shortcutText(shortcuts.sessionCycleRecentReverse())} to cycle through recently visited sessions`
: undefined,
(shortcuts) =>
press(shortcuts.sessionToggleRecent(), "in the session list to show or hide a session in the Recent group"),
] satisfies Tip[])
: []),
"Run {highlight}/compact{/highlight} to summarize long sessions near context limits",
"Press {highlight}Ctrl+X X{/highlight} or {highlight}/export{/highlight} to save the conversation as Markdown",
"Press {highlight}Ctrl+X Y{/highlight} to copy the assistant's last message to clipboard",
"Press {highlight}Ctrl+P{/highlight} to see all available actions and commands",
(shortcuts) => `Use ${commandText("/export", shortcuts.sessionExport())} to save the conversation as Markdown`,
(shortcuts) => press(shortcuts.messagesCopy(), "to copy the assistant's last message to clipboard"),
(shortcuts) => press(shortcuts.commandList(), "to see all available actions and commands"),
"Run {highlight}/connect{/highlight} to add API keys for 75+ supported LLM providers",
"The leader key is {highlight}Ctrl+X{/highlight}; combine with other keys for quick actions",
"Press {highlight}F2{/highlight} to quickly switch between recently used models",
"Press {highlight}Ctrl+X B{/highlight} to show/hide the sidebar panel",
"Use {highlight}PageUp{/highlight}/{highlight}PageDown{/highlight} to navigate through conversation history",
"Press {highlight}Ctrl+G{/highlight} or {highlight}Home{/highlight} to jump to the beginning of the conversation",
"Press {highlight}Ctrl+Alt+G{/highlight} or {highlight}End{/highlight} to jump to the most recent message",
"Press {highlight}Shift+Enter{/highlight} or {highlight}Ctrl+J{/highlight} to add newlines in your prompt",
"Press {highlight}Ctrl+C{/highlight} when typing to clear the input field",
"Press {highlight}Escape{/highlight} to stop the AI mid-response",
(shortcuts) => `The leader key is ${shortcutText(shortcuts.leader())}; combine with other keys for quick actions`,
(shortcuts) => press(shortcuts.modelCycleRecent(), "to quickly switch between recently used models"),
(shortcuts) => press(shortcuts.sessionSidebarToggle(), "in a session to show or hide the sidebar panel"),
(shortcuts) =>
shortcuts.messagesPageUp() && shortcuts.messagesPageDown()
? `Use ${shortcutText(shortcuts.messagesPageUp())}/${shortcutText(shortcuts.messagesPageDown())} to navigate through conversation history`
: undefined,
(shortcuts) => press(shortcuts.messagesFirst(), "to jump to the beginning of the conversation"),
(shortcuts) => press(shortcuts.messagesLast(), "to jump to the most recent message"),
(shortcuts) => press(shortcuts.inputNewline(), "to add newlines in your prompt"),
(shortcuts) => press(shortcuts.inputClear(), "when typing to clear the input field"),
(shortcuts) => press(shortcuts.sessionInterrupt(), "to stop the AI mid-response"),
"Switch to {highlight}Plan{/highlight} agent to get suggestions without making actual changes",
"Use {highlight}@agent-name{/highlight} in prompts to invoke specialized subagents",
"Press {highlight}Ctrl+X Right/Left{/highlight} to cycle through parent and child sessions",
(shortcuts) => {
const items = [
shortcuts.sessionParent(),
shortcuts.childFirst(),
shortcuts.childPrevious(),
shortcuts.childNext(),
].filter(Boolean)
if (!items.length) return undefined
return `Use ${items.map(shortcutText).join(" / ")} to move between parent and child sessions`
},
"Create {highlight}opencode.json{/highlight} for server settings and {highlight}tui.json{/highlight} for TUI settings",
"Place TUI settings in {highlight}~/.config/opencode/tui.json{/highlight} for global config",
"Add {highlight}$schema{/highlight} to your config for autocomplete in your editor",
@@ -99,22 +229,21 @@ const TIPS = [
"Override any keybind in {highlight}tui.json{/highlight} via the {highlight}keybinds{/highlight} section",
"Set any keybind to {highlight}none{/highlight} to disable it completely",
"Configure local or remote MCP servers in the {highlight}mcp{/highlight} config section",
"OpenCode auto-handles OAuth for remote MCP servers requiring auth",
"Add {highlight}.md{/highlight} files to {highlight}.opencode/command/{/highlight} to define reusable custom prompts",
"Add {highlight}.md{/highlight} files to {highlight}.opencode/commands/{/highlight} to define reusable custom prompts",
"Use {highlight}$ARGUMENTS{/highlight}, {highlight}$1{/highlight}, {highlight}$2{/highlight} in custom commands for dynamic input",
"Use backticks in commands to inject shell output (e.g., {highlight}`git status`{/highlight})",
"Add {highlight}.md{/highlight} files to {highlight}.opencode/agent/{/highlight} for specialized AI personas",
"Add {highlight}.md{/highlight} files to {highlight}.opencode/agents/{/highlight} for specialized AI personas",
"Configure per-agent permissions for {highlight}edit{/highlight}, {highlight}bash{/highlight}, and {highlight}webfetch{/highlight} tools",
'Use patterns like {highlight}"git *": "allow"{/highlight} for granular bash permissions',
'Set {highlight}"rm -rf *": "deny"{/highlight} to block destructive commands',
'Configure {highlight}"git push": "ask"{/highlight} to require approval before pushing',
"OpenCode auto-formats files using prettier, gofmt, ruff, and more",
'Set {highlight}"formatter": false{/highlight} in config to disable all auto-formatting',
'Set {highlight}"formatter": true{/highlight} in config to enable built-in formatters like prettier, gofmt, and ruff',
'Set {highlight}"formatter": false{/highlight} in config to disable formatters enabled by another config layer',
"Define custom formatter commands with file extensions in config",
"OpenCode uses LSP servers for intelligent code analysis",
'Set {highlight}"lsp": true{/highlight} in config to enable built-in LSP servers for code analysis',
"Create {highlight}.ts{/highlight} files in {highlight}.opencode/tools/{/highlight} to define new LLM tools",
"Tool definitions can invoke scripts written in Python, Go, etc",
"Add {highlight}.ts{/highlight} files to {highlight}.opencode/plugin/{/highlight} for event hooks",
"Add {highlight}.ts{/highlight} files to {highlight}.opencode/plugins/{/highlight} for event hooks",
"Use plugins to send OS notifications when sessions complete",
"Create a plugin to prevent OpenCode from reading sensitive files",
"Use {highlight}opencode run{/highlight} for non-interactive scripting",
@@ -133,7 +262,7 @@ const TIPS = [
'Use {highlight}"theme": "system"{/highlight} to match your terminal\'s colors',
"Create JSON theme files in {highlight}.opencode/themes/{/highlight} directory",
"Themes support dark/light variants for both modes",
"Reference ANSI colors 0-255 in custom themes",
"Use numeric xterm color codes 0-255 in custom theme JSON",
"Use {highlight}{env:VAR_NAME}{/highlight} syntax to reference environment variables in config",
"Use {highlight}{file:path}{/highlight} to include file contents in config values",
"Use {highlight}instructions{/highlight} in config to load additional rules files",
@@ -149,18 +278,23 @@ const TIPS = [
"Permission {highlight}external_directory{/highlight} protects files outside project",
"Run {highlight}opencode debug config{/highlight} to troubleshoot configuration",
"Use {highlight}--print-logs{/highlight} flag to see detailed logs in stderr",
"Press {highlight}Ctrl+X G{/highlight} or {highlight}/timeline{/highlight} to jump to specific messages",
"Press {highlight}Ctrl+X H{/highlight} to toggle code block visibility in messages",
"Press {highlight}Ctrl+X S{/highlight} or {highlight}/status{/highlight} to see system status info",
(shortcuts) => `Use ${commandText("/timeline", shortcuts.sessionTimeline())} to jump to specific messages`,
(shortcuts) => press(shortcuts.messagesToggleConceal(), "to toggle code block visibility in messages"),
(shortcuts) => `Use ${commandText("/status", shortcuts.statusView())} to see system status info`,
"Enable {highlight}scroll_acceleration{/highlight} in {highlight}tui.json{/highlight} for smooth macOS-style scrolling",
"Toggle username display in chat via command palette ({highlight}Ctrl+P{/highlight})",
(shortcuts) =>
shortcuts.commandList()
? `Toggle username display in chat via the command palette (${shortcutText(shortcuts.commandList())})`
: "Toggle username display in chat via the command palette",
"Run {highlight}docker run -it --rm ghcr.io/anomalyco/opencode{/highlight} for containerized use",
"Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models",
"Commit your project's {highlight}AGENTS.md{/highlight} file to Git for team sharing",
"Use {highlight}/review{/highlight} to review uncommitted changes, branches, or PRs",
"Run {highlight}/help{/highlight} or {highlight}Ctrl+X H{/highlight} to show the help dialog",
(shortcuts) => `Use ${commandText("/help", shortcuts.helpShow())} to show the help dialog`,
"Use {highlight}/rename{/highlight} to rename the current session",
...(process.platform === "win32"
? ["Press {highlight}Ctrl+Z{/highlight} to undo changes in your prompt"]
: ["Press {highlight}Ctrl+Z{/highlight} to suspend the terminal and return to your shell"]),
? ([(shortcuts) => press(shortcuts.inputUndo(), "to undo changes in your prompt")] satisfies Tip[])
: ([
(shortcuts) => press(shortcuts.terminalSuspend(), "to suspend the terminal and return to your shell"),
] satisfies Tip[])),
]
@@ -24,9 +24,9 @@ function View(props: { api: TuiPluginApi; hidden: boolean; show: boolean; connec
}))
return (
<box height={4} minHeight={0} width="100%" maxWidth={75} alignItems="center" paddingTop={3} flexShrink={1}>
<box width="100%" maxWidth={75} alignItems="center" paddingTop={3} flexShrink={1}>
<Show when={props.show}>
<Tips connected={props.connected} />
<Tips api={props.api} connected={props.connected} />
</Show>
</box>
)
@@ -0,0 +1,94 @@
import type { Event } from "@opencode-ai/sdk/v2"
import type { TuiAttentionSoundName, TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
import type { InternalTuiPlugin } from "../../plugin/internal"
const id = "internal:notifications"
type SessionError = Extract<Event, { type: "session.error" }>["properties"]["error"]
function notify(api: TuiPluginApi, sessionID: string | undefined, message: string, sound: TuiAttentionSoundName) {
const session = sessionID ? api.state.session.get(sessionID) : undefined
const isSubagent = session?.parentID !== undefined
void api.attention.notify({
title: session?.title,
message,
notification: isSubagent ? false : { when: "blurred" },
sound: { name: sound, when: "always" },
})
}
function sessionErrorMessage(error: SessionError) {
if (error?.name === "MessageAbortedError") return "Session aborted"
const data = error?.data
if (data && typeof data === "object" && "message" in data && data.message === "SSE read timed out") {
return "Model stopped responding"
}
return "Session error"
}
const tui: TuiPlugin = async (api) => {
const active = new Set<string>()
const errored = new Set<string>()
const questions = new Set<string>()
const permissions = new Set<string>()
api.event.on("question.asked", (event) => {
if (questions.has(event.properties.id)) return
questions.add(event.properties.id)
notify(api, event.properties.sessionID, "Question needs input", "question")
})
api.event.on("question.replied", (event) => {
questions.delete(event.properties.requestID)
})
api.event.on("question.rejected", (event) => {
questions.delete(event.properties.requestID)
})
api.event.on("permission.asked", (event) => {
if (permissions.has(event.properties.id)) return
permissions.add(event.properties.id)
notify(api, event.properties.sessionID, "Permission needs input", "permission")
})
api.event.on("permission.replied", (event) => {
permissions.delete(event.properties.requestID)
})
api.event.on("session.status", (event) => {
const sessionID = event.properties.sessionID
if (event.properties.status.type === "busy" || event.properties.status.type === "retry") {
active.add(sessionID)
errored.delete(sessionID)
return
}
if (event.properties.status.type !== "idle") return
if (!active.has(sessionID)) return
active.delete(sessionID)
if (errored.has(sessionID)) {
errored.delete(sessionID)
return
}
const session = api.state.session.get(sessionID)
notify(api, sessionID, "Session done", session?.parentID ? "subagent_done" : "done")
})
api.event.on("session.error", (event) => {
const sessionID = event.properties.sessionID
if (!sessionID) return
if (!active.has(sessionID)) return
errored.add(sessionID)
notify(api, sessionID, sessionErrorMessage(event.properties.error), "error")
})
}
const plugin: InternalTuiPlugin = {
id,
tui,
}
export default plugin
@@ -40,6 +40,7 @@ type Input = {
theme: ReturnType<typeof useTheme>
toast: ReturnType<typeof useToast>
renderer: TuiPluginApi["renderer"]
attention: TuiPluginApi["attention"]
}
function routeRegister(routes: RouteMap, list: TuiRouteDefinition[], bump: () => void) {
@@ -206,6 +207,7 @@ export function createTuiApi(input: Input): TuiPluginApi {
}
return {
app: appApi(),
attention: input.attention,
// Keep deprecated `api.command` working for v1 plugins; remove in v2.
command: createCommandShim(input.keymap, input.dialog, input.tuiConfig.keybinds),
keys: {
@@ -7,6 +7,7 @@ import SidebarTodo from "../feature-plugins/sidebar/todo"
import SidebarFiles from "../feature-plugins/sidebar/files"
import SidebarFooter from "../feature-plugins/sidebar/footer"
import PluginManager from "../feature-plugins/system/plugins"
import Notifications from "../feature-plugins/system/notifications"
import SessionV2Debug from "../feature-plugins/system/session-v2"
import WhichKey from "../feature-plugins/system/which-key"
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
@@ -27,6 +28,7 @@ export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [
SidebarTodo,
SidebarFiles,
SidebarFooter,
Notifications,
PluginManager,
WhichKey,
...(Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM ? [SessionV2Debug] : []),
@@ -17,6 +17,7 @@ import { TuiConfig } from "@/cli/cmd/tui/config/tui"
import * as Log from "@opencode-ai/core/util/log"
import { errorData, errorMessage } from "@/util/error"
import { isRecord } from "@/util/record"
import { resolveAttentionSoundPaths } from "../config/tui-schema"
import {
readPackageThemes,
readPluginId,
@@ -51,7 +52,7 @@ type PluginLoad = {
id: string
module: TuiPluginModule
origin: ConfigPlugin.Origin
theme_root: string
plugin_root: string
theme_files: string[]
}
@@ -106,6 +107,7 @@ const ScopedKeymapMethods = new Set<PropertyKey>([
type RuntimeState = {
directory: string
api: Api
dispose?: () => void
slots: HostSlots
plugins: PluginEntry[]
plugins_by_id: Map<string, PluginEntry>
@@ -156,6 +158,37 @@ function createScopedKeymap(keymap: TuiPluginApi["keymap"], scope: PluginScope):
})
}
function createScopedAttention(
attention: TuiPluginApi["attention"],
scope: PluginScope,
root: string,
): TuiPluginApi["attention"] {
return {
notify(input) {
return attention.notify(input)
},
soundboard: {
registerPack(pack) {
return scope.track(
attention.soundboard.registerPack({
...pack,
sounds: resolveAttentionSoundPaths(root, pack.sounds, { trim: true }),
}),
)
},
activate(id, options) {
return attention.soundboard.activate(id, options)
},
current() {
return attention.soundboard.current()
},
list() {
return attention.soundboard.list()
},
},
}
}
type CleanupResult = { type: "ok" } | { type: "error"; error: unknown } | { type: "timeout" }
function runCleanup(fn: () => unknown, ms: number): Promise<CleanupResult> {
@@ -204,8 +237,7 @@ function createThemeInstaller(
plugin: PluginEntry,
): TuiTheme["install"] {
return async (file) => {
const raw = file.startsWith("file://") ? fileURLToPath(file) : file
const src = path.isAbsolute(raw) ? raw : path.resolve(root, raw)
const src = Filesystem.resolveFilePath(root, file)
const name = path.basename(src, path.extname(src))
const source_dir = path.dirname(meta.source)
const local_dir =
@@ -330,7 +362,7 @@ function loadInternalPlugin(item: InternalTuiPlugin): PluginLoad {
scope: "global",
source: target,
},
theme_root: process.cwd(),
plugin_root: process.cwd(),
theme_files: [],
}
}
@@ -352,7 +384,7 @@ async function readThemeFiles(spec: string, pkg?: PluginPackage) {
async function syncPluginThemes(plugin: PluginEntry) {
if (!plugin.load.theme_files.length) return
if (plugin.meta.state === "same") return
const install = createThemeInstaller(plugin.load.origin, plugin.load.theme_root, plugin.load.spec, plugin)
const install = createThemeInstaller(plugin.load.origin, plugin.load.plugin_root, plugin.load.spec, plugin)
for (const file of plugin.load.theme_files) {
await install(file).catch((error) => {
warn("failed to sync tui plugin oc-themes", { path: plugin.load.spec, id: plugin.id, theme: file, error })
@@ -552,7 +584,7 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
}
const theme: TuiPluginApi["theme"] = Object.assign(Object.create(api.theme), {
install: createThemeInstaller(load.origin, load.theme_root, load.spec, plugin),
install: createThemeInstaller(load.origin, load.plugin_root, load.spec, plugin),
})
const event: TuiPluginApi["event"] = {
@@ -576,6 +608,7 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
return {
app: api.app,
attention: createScopedAttention(api.attention, scope, load.plugin_root),
// Keep deprecated `api.command` working for v1 plugins; remove in v2.
command: createCommandShim(keymap, api.ui.dialog, api.tuiConfig.keybinds),
keys: api.keys,
@@ -682,7 +715,7 @@ async function resolveExternalPlugins(list: ConfigPlugin.Origin[], wait: () => P
id,
module: mod,
origin,
theme_root: loaded.pkg?.dir ?? resolveRoot(loaded.target),
plugin_root: loaded.pkg?.dir ?? resolveRoot(loaded.target),
theme_files,
}
},
@@ -709,7 +742,7 @@ async function resolveExternalPlugins(list: ConfigPlugin.Origin[], wait: () => P
id,
module: EMPTY_TUI,
origin,
theme_root: loaded.pkg?.dir ?? resolveRoot(loaded.target),
plugin_root: loaded.pkg?.dir ?? resolveRoot(loaded.target),
theme_files,
}
},
@@ -967,7 +1000,7 @@ let loaded: Promise<void> | undefined
let runtime: RuntimeState | undefined
export const Slot = View
export async function init(input: { api: HostPluginApi; config: TuiConfig.Resolved }) {
export async function init(input: { api: HostPluginApi; config: TuiConfig.Resolved; dispose?: () => void }) {
const cwd = process.cwd()
if (loaded) {
if (dir !== cwd) {
@@ -1014,15 +1047,17 @@ export async function dispose() {
for (const plugin of queue) {
await deactivatePluginEntry(state, plugin, false)
}
state.dispose?.()
}
async function load(input: { api: Api; config: TuiConfig.Resolved }) {
async function load(input: { api: Api; config: TuiConfig.Resolved; dispose?: () => void }) {
const { api, config } = input
const cwd = process.cwd()
const slots = setupSlots(api)
const next: RuntimeState = {
directory: cwd,
api,
dispose: input.dispose,
slots,
plugins: [],
plugins_by_id: new Map(),
@@ -0,0 +1,58 @@
import { Audio, type AudioErrorContext, type AudioPlayOptions, type AudioSound, type AudioVoice } from "@opentui/core"
import * as Log from "@opencode-ai/core/util/log"
const log = Log.create({ service: "tui.audio" })
let audio: Audio | null | undefined
const sounds = new Map<string, Promise<AudioSound | null>>()
function getAudio() {
if (audio !== undefined) return audio
try {
const next = Audio.create({ autoStart: false })
next.on("error", (error: Error, context: AudioErrorContext) => {
log.debug("tui audio error", { error, context })
})
audio = next
return next
} catch (error) {
log.debug("failed to create tui audio", { error })
audio = null
return null
}
}
export function loadSoundFile(file: string) {
const current = getAudio()
if (!current) return Promise.resolve(null)
const cached = sounds.get(file)
if (cached) return cached
const task = Bun.file(file)
.bytes()
.then((bytes) => current.loadSound(bytes))
.catch((error) => {
log.debug("failed to load tui sound", { file, error })
return null
})
sounds.set(file, task)
return task
}
export function play(sound: AudioSound, options?: AudioPlayOptions) {
const current = getAudio()
if (!current) return null
if (!current.isStarted() && !current.start()) return null
return current.play(sound, options)
}
export function stopVoice(voice: AudioVoice) {
return audio?.stopVoice(voice) ?? false
}
export function dispose() {
audio?.dispose()
audio = undefined
sounds.clear()
}
export * as TuiAudio from "./audio"
@@ -1,156 +0,0 @@
import { Player } from "cli-sound"
import { mkdirSync } from "node:fs"
import { tmpdir } from "node:os"
import { basename, join } from "node:path"
import { Process } from "@/util/process"
import { which } from "@/util/which"
import pulseA from "../asset/pulse-a.wav" with { type: "file" }
import pulseB from "../asset/pulse-b.wav" with { type: "file" }
import pulseC from "../asset/pulse-c.wav" with { type: "file" }
import charge from "../asset/charge.wav" with { type: "file" }
const FILE = [pulseA, pulseB, pulseC]
const HUM = charge
const DIR = join(tmpdir(), "opencode-sfx")
const LIST = [
"ffplay",
"mpv",
"mpg123",
"mpg321",
"mplayer",
"afplay",
"play",
"omxplayer",
"aplay",
"cmdmp3",
"cvlc",
"powershell.exe",
] as const
type Kind = (typeof LIST)[number]
function args(kind: Kind, file: string, volume: number) {
if (kind === "ffplay") return [kind, "-autoexit", "-nodisp", "-af", `volume=${volume}`, file]
if (kind === "mpv")
return [kind, "--no-video", "--audio-display=no", "--volume", String(Math.round(volume * 100)), file]
if (kind === "mpg123" || kind === "mpg321") return [kind, "-g", String(Math.round(volume * 100)), file]
if (kind === "mplayer") return [kind, "-vo", "null", "-volume", String(Math.round(volume * 100)), file]
if (kind === "afplay" || kind === "omxplayer" || kind === "aplay" || kind === "cmdmp3") return [kind, file]
if (kind === "play") return [kind, "-v", String(volume), file]
if (kind === "cvlc") return [kind, `--gain=${volume}`, "--play-and-exit", file]
return [kind, "-c", `(New-Object Media.SoundPlayer '${file.replace(/'/g, "''")}').PlaySync()`]
}
let item: Player | null | undefined
let kind: Kind | null | undefined
let proc: Process.Child | undefined
let tail: ReturnType<typeof setTimeout> | undefined
let cache: Promise<{ hum: string; pulse: string[] }> | undefined
let seq = 0
let shot = 0
function load() {
if (item !== undefined) return item
try {
item = new Player({ volume: 0.35 })
} catch {
item = null
}
return item
}
async function file(path: string) {
mkdirSync(DIR, { recursive: true })
const next = join(DIR, basename(path))
const out = Bun.file(next)
if (await out.exists()) return next
await Bun.write(out, Bun.file(path))
return next
}
function asset() {
cache ??= Promise.all([file(HUM), Promise.all(FILE.map(file))]).then(([hum, pulse]) => ({ hum, pulse }))
return cache
}
function pick() {
if (kind !== undefined) return kind
kind = LIST.find((item) => which(item)) ?? null
return kind
}
function run(file: string, volume: number) {
const kind = pick()
if (!kind) return
return Process.spawn(args(kind, file, volume), {
stdin: "ignore",
stdout: "ignore",
stderr: "ignore",
})
}
function clear() {
if (!tail) return
clearTimeout(tail)
tail = undefined
}
function play(file: string, volume: number) {
const item = load()
if (!item) return run(file, volume)?.exited
return item.play(file, { volume }).catch(() => run(file, volume)?.exited)
}
export function start() {
stop()
const id = ++seq
void asset().then(({ hum }) => {
if (id !== seq) return
const next = run(hum, 0.24)
if (!next) return
proc = next
void next.exited.then(
() => {
if (id !== seq) return
if (proc === next) proc = undefined
},
() => {
if (id !== seq) return
if (proc === next) proc = undefined
},
)
})
}
export function stop(delay = 0) {
seq++
clear()
if (!proc) return
const next = proc
if (delay <= 0) {
proc = undefined
void Process.stop(next).catch(() => undefined)
return
}
tail = setTimeout(() => {
tail = undefined
if (proc === next) proc = undefined
void Process.stop(next).catch(() => undefined)
}, delay)
}
export function pulse(scale = 1) {
stop(140)
const index = shot++ % FILE.length
void asset()
.then(({ pulse }) => play(pulse[index], 0.26 + 0.14 * scale))
.catch(() => undefined)
}
export function dispose() {
stop()
}
export * as Sound from "./sound"
+2 -2
View File
@@ -78,8 +78,8 @@ export function FormatError(input: unknown) {
].join("\n")
}
// UICancelledError: void (no data)
if (NamedError.hasName(input, "UICancelledError")) {
// UICancelledError: user cancelled an interactive CLI prompt
if (isTaggedError(input, "UICancelledError") || NamedError.hasName(input, "UICancelledError")) {
return ""
}
}
+1 -2
View File
@@ -1,5 +1,4 @@
import { EOL } from "os"
import { NamedError } from "@opencode-ai/core/util/error"
import { Schema } from "effect"
import { logo as glyphs } from "./logo"
@@ -10,7 +9,7 @@ const wordmark = [
`▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`,
]
export const CancelledError = NamedError.create("UICancelledError", Schema.optional(Schema.Void))
export class CancelledError extends Schema.TaggedErrorClass<CancelledError>()("UICancelledError", {}) {}
export const Style = {
TEXT_HIGHLIGHT: "\x1b[96m",
+23 -6
View File
@@ -55,6 +55,16 @@ function mergeConfigConcatArrays(target: Info, source: Info): Info {
if (target.instructions && source.instructions) {
merged.instructions = Array.from(new Set([...target.instructions, ...source.instructions]))
}
// Accumulate permission layers for later merging as rulesets.
// This preserves the ordering semantics: later rules override earlier rules.
// Each layer keeps the raw shape the user wrote on disk; consumers should use
// ConfigPermission.toLayers to normalise.
if (source.permission) {
merged.permission = [
...ConfigPermission.toLayers(target.permission),
...ConfigPermission.toLayers(source.permission),
]
}
return merged
}
@@ -228,7 +238,12 @@ export const Info = Schema.Struct({
description: "Additional instruction files or patterns to include",
}),
layout: Schema.optional(ConfigLayout.Layout).annotate({ description: "@deprecated Always uses stretch layout." }),
permission: Schema.optional(ConfigPermission.Info),
permission: Schema.optional(
Schema.Union([ConfigPermission.Info, Schema.mutable(Schema.Array(ConfigPermission.Info))]),
).annotate({
description:
"Permission configuration. Accepts a single object (per-tool action map) or an array of layered configs; arrays are merged in order so later layers override earlier ones.",
}),
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
attachment: Schema.optional(ConfigAttachment.Info).annotate({
description: "Attachment processing configuration, including image size limits and resizing behavior",
@@ -261,10 +276,10 @@ export const Info = Schema.Struct({
}),
tail_turns: Schema.optional(NonNegativeInt).annotate({
description:
"Number of recent user turns, including their following assistant/tool responses, to serialize into the compaction summary (default: 2)",
"Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2)",
}),
preserve_recent_tokens: Schema.optional(NonNegativeInt).annotate({
description: "Maximum number of tokens from recent turns to serialize into the compaction summary",
description: "Maximum number of tokens from recent turns to preserve verbatim after compaction",
}),
reserved: Schema.optional(NonNegativeInt).annotate({
description: "Token buffer for compaction. Leaves enough window to avoid overflow during compaction.",
@@ -704,11 +719,12 @@ export const layer = Layer.effect(
}
if (Flag.OPENCODE_PERMISSION) {
result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION))
const envPermission = JSON.parse(Flag.OPENCODE_PERMISSION) as ConfigPermission.Info
result.permission = [...ConfigPermission.toLayers(result.permission), envPermission]
}
if (result.tools) {
const perms: Record<string, ConfigPermission.Action> = {}
const perms: ConfigPermission.Info = {}
for (const [tool, enabled] of Object.entries(result.tools)) {
const action: ConfigPermission.Action = enabled ? "allow" : "deny"
if (tool === "write" || tool === "edit" || tool === "patch") {
@@ -717,7 +733,8 @@ export const layer = Layer.effect(
}
perms[tool] = action
}
result.permission = mergeDeep(perms, result.permission ?? {})
// Tools permissions come before other permissions (they can be overridden)
result.permission = [perms, ...ConfigPermission.toLayers(result.permission)]
}
if (!result.username) result.username = os.userInfo().username
@@ -56,3 +56,11 @@ export const Info = InputSchema.pipe(
).annotate({ identifier: "PermissionConfig" })
type _Info = Schema.Schema.Type<typeof InputObject>
export type Info = { -readonly [K in keyof _Info]: _Info[K] }
// Top-level config accepts either a single permission object or an array of
// layered configs. Internal merging produces arrays; this helper normalises
// either shape into the array form expected by consumers.
export function toLayers(value: Info | Info[] | undefined): Info[] {
if (!value) return []
return Array.isArray(value) ? value : [value]
}
+4 -1
View File
@@ -94,6 +94,7 @@ export const layer = Layer.effect(
tokens_reasoning: value.tokens.reasoning,
tokens_cache_read: value.tokens.cache.read,
tokens_cache_write: value.tokens.cache.write,
time_updated: sql`${SessionTable.time_updated}`,
})
.where(eq(SessionTable.id, sessionID))
.run()
@@ -145,7 +146,9 @@ export const layer = Layer.effect(
)
}
}).pipe(
Effect.tapCause((cause) => Effect.logError("failed to run data migrations", { cause })),
Effect.tapCause((cause) =>
Effect.logError("failed to run data migrations").pipe(Effect.annotateLogs("cause", cause)),
),
Effect.ignore,
Effect.forkScoped,
)
+1 -1
View File
@@ -181,7 +181,7 @@ export const make = <A, E = never>(
return [
Effect.gen(function* () {
yield* Fiber.interrupt(st.run.fiber)
yield* Deferred.await(st.run.done).pipe(Effect.exit, Effect.asVoid)
yield* Deferred.fail(st.run.done, new Cancelled()).pipe(Effect.asVoid)
yield* idleIfCurrent()
}),
{ _tag: "Idle" } as const,
@@ -0,0 +1,46 @@
import { Config, ConfigProvider, Context, Effect, Layer } from "effect"
import { ConfigService } from "@/effect/config-service"
const bool = (name: string) => Config.boolean(name).pipe(Config.withDefault(false))
const experimental = bool("OPENCODE_EXPERIMENTAL")
const enabledByExperimental = (name: string) =>
Config.all({ experimental, enabled: bool(name) }).pipe(Config.map((flags) => flags.experimental || flags.enabled))
export class Service extends ConfigService.Service<Service>()("@opencode/RuntimeFlags", {
pure: bool("OPENCODE_PURE"),
disableDefaultPlugins: bool("OPENCODE_DISABLE_DEFAULT_PLUGINS"),
enableExa: Config.all({
experimental,
enabled: bool("OPENCODE_ENABLE_EXA"),
legacy: bool("OPENCODE_EXPERIMENTAL_EXA"),
}).pipe(Config.map((flags) => flags.experimental || flags.enabled || flags.legacy)),
enableParallel: Config.all({
enabled: bool("OPENCODE_ENABLE_PARALLEL"),
legacy: bool("OPENCODE_EXPERIMENTAL_PARALLEL"),
}).pipe(Config.map((flags) => flags.enabled || flags.legacy)),
enableQuestionTool: bool("OPENCODE_ENABLE_QUESTION_TOOL"),
experimentalScout: enabledByExperimental("OPENCODE_EXPERIMENTAL_SCOUT"),
experimentalLspTool: enabledByExperimental("OPENCODE_EXPERIMENTAL_LSP_TOOL"),
experimentalPlanMode: enabledByExperimental("OPENCODE_EXPERIMENTAL_PLAN_MODE"),
client: Config.string("OPENCODE_CLIENT").pipe(Config.withDefault("cli")),
}) {}
export type Info = Context.Service.Shape<typeof Service>
const emptyConfigLayer = Service.defaultLayer.pipe(
Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({}))),
Layer.orDie,
)
export const layer = (overrides: Partial<Info> = {}) =>
Layer.effect(
Service,
Effect.gen(function* () {
const flags = yield* Service
return Service.of({ ...flags, ...overrides })
}),
).pipe(Layer.provide(emptyConfigLayer))
export const defaultLayer = Service.defaultLayer.pipe(Layer.orDie)
export * as RuntimeFlags from "./runtime-flags"
+7 -9
View File
@@ -6,6 +6,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
import {
CallToolResultSchema,
ListToolsResultSchema,
ToolSchema,
type Tool as MCPToolDef,
ToolListChangedNotificationSchema,
@@ -14,7 +15,6 @@ import { Config } from "@/config/config"
import { ConfigMCP } from "../config/mcp"
import * as Log from "@opencode-ai/core/util/log"
import { NamedError } from "@opencode-ai/core/util/error"
import z from "zod/v4"
import { Installation } from "../installation"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { withTimeout } from "@/util/timeout"
@@ -35,13 +35,8 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
const log = Log.create({ service: "mcp" })
const DEFAULT_TIMEOUT = 30_000
const TolerantToolSchema = ToolSchema.extend({
outputSchema: z.unknown().optional(),
})
const TolerantListToolsResultSchema = z.looseObject({
tools: z.array(TolerantToolSchema),
nextCursor: z.string().optional(),
const TolerantListToolsResultSchema = ListToolsResultSchema.extend({
tools: ToolSchema.omit({ outputSchema: true }).array(),
})
export const Resource = Schema.Struct({
@@ -137,7 +132,10 @@ function listTools(key: string, client: MCPClient, timeout: number) {
log.warn("failed to validate MCP tool output schemas, retrying without output schema validation", { key, error })
return Effect.tryPromise({
try: () => client.request({ method: "tools/list" }, TolerantListToolsResultSchema, { timeout }),
try: () =>
client.request({ method: "tools/list" }, TolerantListToolsResultSchema, {
timeout,
}),
catch: (err) => (err instanceof Error ? err : new Error(String(err))),
}).pipe(
Effect.map((result) =>
@@ -0,0 +1,411 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import type { Model } from "@opencode-ai/sdk/v2"
import * as Log from "@opencode-ai/core/util/log"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { createServer } from "http"
const log = Log.create({ service: "plugin.digitalocean" })
const DO_OAUTH_CLIENT_ID = "b1a6c5158156caac821fd1b30253ca8acb52454a48fa744420e41889cb589f82"
const DO_AUTHORIZE_URL = "https://cloud.digitalocean.com/v1/oauth/authorize"
const DO_API_BASE = "https://api.digitalocean.com"
const DO_INFERENCE_BASE = "https://inference.do-ai.run/v1"
const OAUTH_PORT = 1456
const OAUTH_REDIRECT_PATH = "/auth/callback"
const OAUTH_TOKEN_PATH = "/auth/token"
const ROUTER_REFRESH_INTERVAL_MS = 5 * 60 * 1000
const MAK_NAME_PREFIX = "opencode-oauth"
interface ImplicitTokenPayload {
access_token: string
expires_in: number
state: string
}
interface PendingOAuth {
state: string
resolve: (tokens: ImplicitTokenPayload) => void
reject: (error: Error) => void
}
interface ApiKeyInfo {
uuid: string
name: string
secret_key: string
}
interface RouterEntry {
name: string
uuid?: string
description?: string
}
let oauthServer: ReturnType<typeof createServer> | undefined
let pendingOAuth: PendingOAuth | undefined
function generateState(): string {
const bytes = crypto.getRandomValues(new Uint8Array(32))
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
}
function redirectUri(): string {
return `http://localhost:${OAUTH_PORT}${OAUTH_REDIRECT_PATH}`
}
function buildAuthorizeUrl(state: string): string {
const params = new URLSearchParams({
response_type: "token",
client_id: DO_OAUTH_CLIENT_ID,
redirect_uri: redirectUri(),
scope: "read write",
state,
})
return `${DO_AUTHORIZE_URL}?${params.toString()}`
}
const HTML_CALLBACK = `<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>OpenCode - DigitalOcean Authorization</title>
<style>
body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0b1220; color: #e8eef9; }
.container { text-align: center; padding: 2rem; max-width: 32rem; }
h1 { color: #e8eef9; margin-bottom: 1rem; }
p { color: #9aa9c0; }
.error { color: #ff917b; font-family: monospace; margin-top: 1rem; padding: 1rem; background: #3c140d; border-radius: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<h1 id="title">Finishing sign-in...</h1>
<p id="msg">You can close this window once it says you're signed in.</p>
</div>
<script>
(async function() {
const params = new URLSearchParams((window.location.hash || "").slice(1))
const search = new URLSearchParams(window.location.search)
const error = params.get("error") || search.get("error")
const errorDescription = params.get("error_description") || search.get("error_description")
const titleEl = document.getElementById("title")
const msgEl = document.getElementById("msg")
try {
const body = error
? { error, error_description: errorDescription || "" }
: { access_token: params.get("access_token") || "", expires_in: params.get("expires_in") || "0", state: params.get("state") || "" }
await fetch(${JSON.stringify(OAUTH_TOKEN_PATH)}, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
})
if (error) {
titleEl.textContent = "Authorization Failed"
msgEl.textContent = errorDescription || error
msgEl.className = "error"
return
}
titleEl.textContent = "Authorization Successful"
msgEl.textContent = "You can close this window and return to OpenCode."
setTimeout(function () { window.close() }, 2000)
} catch (e) {
titleEl.textContent = "Authorization Failed"
msgEl.textContent = String(e && e.message ? e.message : e)
msgEl.className = "error"
}
})()
</script>
</body>
</html>`
async function startOAuthServer(): Promise<void> {
if (oauthServer) return
oauthServer = createServer((req, res) => {
const url = new URL(req.url || "/", `http://localhost:${OAUTH_PORT}`)
if (req.method === "GET" && url.pathname === OAUTH_REDIRECT_PATH) {
res.writeHead(200, { "Content-Type": "text/html" })
res.end(HTML_CALLBACK)
return
}
if (req.method === "POST" && url.pathname === OAUTH_TOKEN_PATH) {
const chunks: Buffer[] = []
req.on("data", (chunk: Buffer) => chunks.push(chunk))
req.on("end", () => {
const raw = Buffer.concat(chunks).toString("utf8")
let body: Record<string, string> = {}
try {
body = raw ? JSON.parse(raw) : {}
} catch {
body = {}
}
if (!pendingOAuth) {
res.writeHead(409, { "Content-Type": "application/json" })
res.end(JSON.stringify({ error: "no_pending_oauth" }))
return
}
if (body.error) {
const message = body.error_description || body.error || "OAuth error"
pendingOAuth.reject(new Error(String(message)))
pendingOAuth = undefined
res.writeHead(200, { "Content-Type": "application/json" })
res.end(JSON.stringify({ ok: true }))
return
}
if (!body.access_token) {
pendingOAuth.reject(new Error("Missing access_token in callback"))
pendingOAuth = undefined
res.writeHead(400, { "Content-Type": "application/json" })
res.end(JSON.stringify({ error: "missing_access_token" }))
return
}
if (body.state !== pendingOAuth.state) {
pendingOAuth.reject(new Error("Invalid state - potential CSRF attack"))
pendingOAuth = undefined
res.writeHead(400, { "Content-Type": "application/json" })
res.end(JSON.stringify({ error: "invalid_state" }))
return
}
const expires = parseInt(body.expires_in || "0", 10)
pendingOAuth.resolve({
access_token: body.access_token,
expires_in: Number.isFinite(expires) && expires > 0 ? expires : 60 * 60 * 24 * 30,
state: body.state,
})
pendingOAuth = undefined
res.writeHead(200, { "Content-Type": "application/json" })
res.end(JSON.stringify({ ok: true }))
})
return
}
res.writeHead(404)
res.end("Not found")
})
await new Promise<void>((resolve, reject) => {
oauthServer!.listen(OAUTH_PORT, () => {
log.info("digitalocean oauth server started", { port: OAUTH_PORT })
resolve()
})
oauthServer!.on("error", reject)
})
}
function stopOAuthServer() {
if (!oauthServer) return
oauthServer.close(() => log.info("digitalocean oauth server stopped"))
oauthServer = undefined
}
function waitForOAuthCallback(state: string): Promise<ImplicitTokenPayload> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(
() => {
if (pendingOAuth) {
pendingOAuth = undefined
reject(new Error("OAuth callback timeout - authorization took too long"))
}
},
5 * 60 * 1000,
)
pendingOAuth = {
state,
resolve: (tokens) => {
clearTimeout(timeout)
resolve(tokens)
},
reject: (error) => {
clearTimeout(timeout)
reject(error)
},
}
})
}
async function createModelAccessKey(bearer: string): Promise<ApiKeyInfo> {
// Suffix-on-collision strategy keeps re-`/connect` non-destructive.
const name = `${MAK_NAME_PREFIX}-${Math.floor(Date.now() / 1000)}`
const res = await fetch(`${DO_API_BASE}/v2/gen-ai/models/api_keys`, {
method: "POST",
headers: {
Authorization: `Bearer ${bearer}`,
"Content-Type": "application/json",
"User-Agent": `opencode/${InstallationVersion}`,
},
body: JSON.stringify({ name }),
})
if (!res.ok) {
const body = await res.text().catch(() => "")
throw new Error(`Failed to create Model Access Key (${res.status}): ${body}`)
}
const data = (await res.json()) as { api_key_info?: ApiKeyInfo }
if (!data.api_key_info?.secret_key) throw new Error("Model Access Key response missing secret_key")
return data.api_key_info
}
async function listRouters(
bearer: string,
): Promise<{ ok: true; routers: RouterEntry[] } | { ok: false; status: number }> {
const res = await fetch(`${DO_API_BASE}/v2/gen-ai/models/routers`, {
headers: {
Authorization: `Bearer ${bearer}`,
Accept: "application/json",
"User-Agent": `opencode/${InstallationVersion}`,
},
signal: AbortSignal.timeout(10_000),
}).catch(() => undefined)
if (!res) return { ok: false, status: 0 }
if (!res.ok) return { ok: false, status: res.status }
const body = (await res.json().catch(() => undefined)) as { model_routers?: RouterEntry[] } | undefined
return { ok: true, routers: body?.model_routers ?? [] }
}
function routerModel(router: RouterEntry, providerID: string): Model {
const id = `router:${router.name}`
return {
id,
providerID,
name: router.name,
family: "digitalocean-inference-routers",
api: { id, url: DO_INFERENCE_BASE, npm: "@ai-sdk/openai-compatible" },
status: "active",
headers: {},
options: {},
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: 128_000, output: 8_192 },
capabilities: {
temperature: true,
reasoning: false,
attachment: false,
toolcall: true,
input: { text: true, audio: false, image: false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
release_date: "",
variants: {},
}
}
function parseRoutersJSON(raw: string | undefined): RouterEntry[] {
if (!raw) return []
try {
const parsed = JSON.parse(raw)
if (!Array.isArray(parsed)) return []
return parsed.flatMap((r) =>
r && typeof r.name === "string" ? [{ name: r.name, uuid: r.uuid, description: r.description }] : [],
)
} catch {
return []
}
}
export async function DigitalOceanAuthPlugin(input: PluginInput): Promise<Hooks> {
return {
provider: {
id: "digitalocean",
async models(provider, ctx) {
const baseModels = provider.models
if (ctx.auth?.type !== "api") return baseModels
const metadata = ctx.auth.metadata ?? {}
const oauthAccess = metadata["oauth_access"]
const oauthExpires = parseInt(metadata["oauth_expires"] || "0", 10)
const fetchedAt = parseInt(metadata["routers_fetched_at"] || "0", 10)
const cached = parseRoutersJSON(metadata["routers"])
let routers = cached
const stale = Date.now() - fetchedAt > ROUTER_REFRESH_INTERVAL_MS
const bearerValid = oauthAccess && oauthExpires > Date.now()
if (bearerValid && stale) {
const result = await listRouters(oauthAccess)
if (result.ok) {
routers = result.routers
const updated: Record<string, string> = {
...metadata,
routers: JSON.stringify(routers.map((r) => ({ name: r.name, uuid: r.uuid, description: r.description }))),
routers_fetched_at: String(Date.now()),
}
await input.client.auth
.set({
path: { id: "digitalocean" },
body: { type: "api", key: ctx.auth.key, metadata: updated },
})
.catch((err) => log.warn("failed to persist refreshed routers", { error: err }))
} else if (result.status === 401 || result.status === 403) {
log.warn("digitalocean oauth bearer rejected; using cached routers", { status: result.status })
} else if (result.status !== 0) {
log.warn("digitalocean router refresh failed", { status: result.status })
}
}
const merged: Record<string, Model> = { ...baseModels }
for (const router of routers) {
const id = `router:${router.name}`
if (merged[id]) continue
merged[id] = routerModel(router, "digitalocean")
}
return merged
},
},
auth: {
provider: "digitalocean",
methods: [
{
type: "oauth",
label: "Login with DigitalOcean",
async authorize() {
await startOAuthServer()
const state = generateState()
const callbackPromise = waitForOAuthCallback(state)
return {
url: buildAuthorizeUrl(state),
instructions:
"Sign in to DigitalOcean in your browser. OpenCode will create a Model Access Key named opencode-oauth-* and load your Inference Routers. Re-run /connect to refresh routers later.",
method: "auto" as const,
async callback() {
try {
const tokens = await callbackPromise
const apiKeyInfo = await createModelAccessKey(tokens.access_token)
const routerResult = await listRouters(tokens.access_token)
const routers = routerResult.ok ? routerResult.routers : []
if (!routerResult.ok) {
log.warn("digitalocean initial router fetch failed", { status: routerResult.status })
}
return {
type: "success" as const,
provider: "digitalocean",
key: apiKeyInfo.secret_key,
metadata: {
mak_uuid: apiKeyInfo.uuid,
mak_name: apiKeyInfo.name,
oauth_access: tokens.access_token,
oauth_expires: String(Date.now() + tokens.expires_in * 1000),
routers: JSON.stringify(
routers.map((r) => ({ name: r.name, uuid: r.uuid, description: r.description })),
),
routers_fetched_at: String(Date.now()),
},
}
} catch (err) {
log.error("digitalocean oauth callback failed", { error: err })
return { type: "failed" as const }
} finally {
stopOAuthServer()
}
},
}
},
},
{
type: "api",
label: "Paste Model Access Key",
},
],
},
}
}
+12 -5
View File
@@ -9,7 +9,6 @@ import { Config } from "@/config/config"
import { Bus } from "../bus"
import * as Log from "@opencode-ai/core/util/log"
import { createOpencodeClient } from "@opencode-ai/sdk"
import { Flag } from "@opencode-ai/core/flag/flag"
import { ServerAuth } from "@/server/auth"
import { CodexAuthPlugin } from "./codex"
import { Session } from "@/session/session"
@@ -19,6 +18,7 @@ import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth"
import { PoeAuthPlugin } from "opencode-poe-auth"
import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare"
import { AzureAuthPlugin } from "./azure"
import { DigitalOceanAuthPlugin } from "./digitalocean"
import { Effect, Layer, Context, Stream } from "effect"
import { EffectBridge } from "@/effect/bridge"
import { InstanceState } from "@/effect/instance-state"
@@ -27,6 +27,7 @@ import { PluginLoader } from "./loader"
import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared"
import { registerAdapter } from "@/control-plane/adapters"
import type { WorkspaceAdapter } from "@/control-plane/types"
import { RuntimeFlags } from "@/effect/runtime-flags"
const log = Log.create({ service: "plugin" })
@@ -64,6 +65,7 @@ const INTERNAL_PLUGINS: PluginInstance[] = [
CloudflareWorkersAuthPlugin,
CloudflareAIGatewayAuthPlugin,
AzureAuthPlugin,
DigitalOceanAuthPlugin,
]
function isServerPlugin(value: unknown): value is PluginInstance {
@@ -110,6 +112,7 @@ export const layer = Layer.effect(
Effect.gen(function* () {
const bus = yield* Bus.Service
const config = yield* Config.Service
const flags = yield* RuntimeFlags.Service
const state = yield* InstanceState.make<State>(
Effect.fn("Plugin.state")(function* (ctx) {
@@ -146,7 +149,7 @@ export const layer = Layer.effect(
$: typeof Bun === "undefined" ? undefined : Bun.$,
}
for (const plugin of INTERNAL_PLUGINS) {
for (const plugin of flags.disableDefaultPlugins ? [] : INTERNAL_PLUGINS) {
log.info("loading internal plugin", { name: plugin.name })
const init = yield* Effect.tryPromise({
try: () => plugin(input),
@@ -157,8 +160,8 @@ export const layer = Layer.effect(
if (init._tag === "Some") hooks.push(init.value)
}
const plugins = Flag.OPENCODE_PURE ? [] : (cfg.plugin_origins ?? [])
if (Flag.OPENCODE_PURE && cfg.plugin_origins?.length) {
const plugins = flags.pure ? [] : (cfg.plugin_origins ?? [])
if (flags.pure && cfg.plugin_origins?.length) {
log.info("skipping external plugins in pure mode", { count: cfg.plugin_origins.length })
}
if (plugins.length) yield* config.waitForDependencies()
@@ -283,6 +286,10 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Config.defaultLayer))
export const defaultLayer = layer.pipe(
Layer.provide(Bus.layer),
Layer.provide(Config.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
)
export * as Plugin from "."
+1 -1
View File
@@ -37,7 +37,7 @@ export const layer = Layer.effect(
const run = Effect.gen(function* () {
const ctx = yield* InstanceState.context
yield* Effect.logInfo("bootstrapping", { directory: ctx.directory })
yield* Effect.logInfo("bootstrapping").pipe(Effect.annotateLogs("directory", ctx.directory))
// everything depends on config so eager load it for nice traces
yield* config.get()
// Plugin can mutate config so it has to be initialized before anything else.
@@ -156,7 +156,9 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
Effect.gen(function* () {
const exit = yield* Deferred.await(item[1].deferred).pipe(Effect.exit)
if (Exit.isFailure(exit)) {
yield* Effect.logWarning("instance dispose failed", { key: item[0], cause: exit.cause })
yield* Effect.logWarning("instance dispose failed").pipe(
Effect.annotateLogs({ key: item[0], cause: exit.cause }),
)
yield* removeEntry(item[0], item[1])
return
}
+6 -5
View File
@@ -1,7 +1,7 @@
import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
import { Auth } from "@/auth"
import { InstanceState } from "@/effect/instance-state"
import { namedSchemaError } from "@/util/named-schema-error"
import { NamedError } from "@opencode-ai/core/util/error"
import { optionalOmitUndefined } from "@opencode-ai/core/schema"
import { Plugin } from "../plugin"
import { ProviderID } from "./schema"
@@ -64,13 +64,13 @@ export const CallbackInput = Schema.Struct({
})
export type CallbackInput = Schema.Schema.Type<typeof CallbackInput>
export const OauthMissing = namedSchemaError("ProviderAuthOauthMissing", { providerID: ProviderID })
export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", { providerID: ProviderID })
export const OauthCodeMissing = namedSchemaError("ProviderAuthOauthCodeMissing", { providerID: ProviderID })
export const OauthCodeMissing = NamedError.create("ProviderAuthOauthCodeMissing", { providerID: ProviderID })
export const OauthCallbackFailed = namedSchemaError("ProviderAuthOauthCallbackFailed", {})
export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", {})
export const ValidationFailed = namedSchemaError("ProviderAuthValidationFailed", {
export const ValidationFailed = NamedError.create("ProviderAuthValidationFailed", {
field: Schema.String,
message: Schema.String,
})
@@ -197,6 +197,7 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
yield* auth.set(input.providerID, {
type: "api",
key: result.key,
...(result.metadata ? { metadata: result.metadata } : {}),
})
}
+15 -1
View File
@@ -10,11 +10,23 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { withTransientReadRetry } from "@/util/effect-http-client"
import { CatalogModelStatus } from "./model-status"
const CostTier = Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
cache_read: Schema.optional(Schema.Finite),
cache_write: Schema.optional(Schema.Finite),
tier: Schema.Struct({
type: Schema.Literal("context"),
size: Schema.Finite,
}),
})
const Cost = Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
cache_read: Schema.optional(Schema.Finite),
cache_write: Schema.optional(Schema.Finite),
tiers: Schema.optional(Schema.Array(CostTier)),
context_over_200k: Schema.optional(
Schema.Struct({
input: Schema.Finite,
@@ -177,7 +189,9 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | HttpClie
yield* invalidate
}),
).pipe(
Effect.tapCause((cause) => Effect.logError("Failed to fetch models.dev", { cause })),
Effect.tapCause((cause) =>
Effect.logError("Failed to fetch models.dev").pipe(Effect.annotateLogs("cause", cause)),
),
Effect.ignore,
)
})
+25 -3
View File
@@ -13,7 +13,7 @@ import { Auth } from "../auth"
import { Env } from "../env"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { Flag } from "@opencode-ai/core/flag/flag"
import { namedSchemaError } from "@/util/named-schema-error"
import { NamedError } from "@opencode-ai/core/util/error"
import { iife } from "@/util/iife"
import { Global } from "@opencode-ai/core/global"
import path from "path"
@@ -869,10 +869,21 @@ const ProviderCacheCost = Schema.Struct({
write: Schema.Finite,
})
const ProviderCostTier = Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
cache: ProviderCacheCost,
tier: Schema.Struct({
type: Schema.Literal("context"),
size: Schema.Finite,
}),
})
const ProviderCost = Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
cache: ProviderCacheCost,
tiers: optionalOmitUndefined(Schema.Array(ProviderCostTier)),
experimentalOver200K: optionalOmitUndefined(
Schema.Struct({
input: Schema.Finite,
@@ -977,6 +988,17 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
write: c?.cache_write ?? 0,
},
}
if (c?.tiers) {
result.tiers = c.tiers.map((item) => ({
input: item.input,
output: item.output,
cache: {
read: item.cache_read ?? 0,
write: item.cache_write ?? 0,
},
tier: item.tier,
}))
}
if (c?.context_over_200k) {
result.experimentalOver200K = {
cache: {
@@ -1749,13 +1771,13 @@ export function parseModel(model: string) {
}
}
export const ModelNotFoundError = namedSchemaError("ProviderModelNotFoundError", {
export const ModelNotFoundError = NamedError.create("ProviderModelNotFoundError", {
providerID: ProviderID,
modelID: ModelID,
suggestions: Schema.optional(Schema.Array(Schema.String)),
})
export const InitError = namedSchemaError("ProviderInitError", {
export const InitError = NamedError.create("ProviderInitError", {
providerID: ProviderID,
})
+3 -1
View File
@@ -169,7 +169,9 @@ export const layer = Layer.effect(
).pipe(
Effect.asVoid,
Effect.catchCause((cause) =>
Effect.logWarning("failed to materialize reference repository", { name: reference.name, cause }),
Effect.logWarning("failed to materialize reference repository").pipe(
Effect.annotateLogs({ name: reference.name, cause }),
),
),
),
)
@@ -79,7 +79,7 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
const list = yield* registry.tools({
providerID: ctx.query.provider,
modelID: ctx.query.model,
agent: yield* agents.get(yield* agents.defaultAgent()),
agent: yield* agents.defaultInfo(),
})
return list.map((item) => ({
id: item.id,
@@ -1,5 +1,3 @@
import * as InstanceState from "@/effect/instance-state"
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
import { Agent } from "@/agent/agent"
import { Bus } from "@/bus"
import { Command } from "@/command"
@@ -275,18 +273,12 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
params: { sessionID: SessionID }
payload: typeof PromptPayload.Type
}) {
const instance = yield* InstanceState.context
const workspace = yield* InstanceState.workspaceID
const message = yield* promptSvc
.prompt({
...ctx.payload,
sessionID: ctx.params.sessionID,
})
.pipe(
Effect.provideService(InstanceRef, instance),
Effect.provideService(WorkspaceRef, workspace),
Effect.mapError(() => new HttpApiError.BadRequest({})),
)
.pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
return HttpServerResponse.stream(Stream.make(JSON.stringify(message)).pipe(Stream.encodeText), {
contentType: "application/json",
})
@@ -299,7 +291,9 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
yield* promptSvc.prompt({ ...ctx.payload, sessionID: ctx.params.sessionID }).pipe(
Effect.catchCause((cause) =>
Effect.gen(function* () {
yield* Effect.logError("prompt_async failed", { sessionID: ctx.params.sessionID, cause })
yield* Effect.logError("prompt_async failed").pipe(
Effect.annotateLogs({ sessionID: ctx.params.sessionID, cause }),
)
yield* bus.publish(Session.Event.Error, {
sessionID: ctx.params.sessionID,
error: new NamedError.Unknown({ message: Cause.pretty(cause) }).toObject(),
+24 -36
View File
@@ -79,10 +79,12 @@ Rules:
type Turn = {
start: number
end: number
id: MessageID
}
type Tail = {
start: number
id: MessageID
}
type CompletedCompaction = {
@@ -119,41 +121,19 @@ function completedCompactions(messages: MessageV2.WithParts[]) {
})
}
function buildPrompt(input: { previousSummary?: string; context: string[]; tail?: string }) {
const source = input.tail
? "the conversation history above and the serialized recent conversation tail below"
: "the conversation history above"
function buildPrompt(input: { previousSummary?: string; context: string[] }) {
const anchor = input.previousSummary
? [
`Update the anchored summary below using ${source}.`,
"Update the anchored summary below using the conversation history above.",
"Preserve still-true details, remove stale details, and merge in the new facts.",
"<previous-summary>",
input.previousSummary,
"</previous-summary>",
].join("\n")
: `Create a new anchored summary from ${source}.`
const tail = input.tail
? [
"Fold this serialized recent conversation tail into the summary; it is not provider message history.",
"<recent-conversation-tail>",
input.tail,
"</recent-conversation-tail>",
].join("\n")
: undefined
return [anchor, ...(tail ? [tail] : []), SUMMARY_TEMPLATE, ...input.context].join("\n\n")
: "Create a new anchored summary from the conversation history above."
return [anchor, SUMMARY_TEMPLATE, ...input.context].join("\n\n")
}
const serialize = Effect.fn("SessionCompaction.serialize")(function* (input: {
messages: MessageV2.WithParts[]
model: Provider.Model
}) {
const messages = yield* MessageV2.toModelMessagesEffect(input.messages, input.model, {
stripMedia: true,
toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
})
return messages.length ? JSON.stringify(messages, null, 2) : undefined
})
function preserveRecentBudget(input: { cfg: Config.Info; model: Provider.Model }) {
return (
input.cfg.compaction?.preserve_recent_tokens ??
@@ -170,6 +150,7 @@ function turns(messages: MessageV2.WithParts[]) {
result.push({
start: i,
end: messages.length,
id: msg.info.id,
})
}
for (let i = 0; i < result.length - 1; i++) {
@@ -196,6 +177,7 @@ function splitTurn(input: {
if (size > input.budget) continue
return {
start,
id: input.messages[start]!.info.id,
} satisfies Tail
}
return undefined
@@ -262,7 +244,8 @@ export const layer: Layer.Layer<
messages: MessageV2.WithParts[]
model: Provider.Model
}) {
return Token.estimate((yield* serialize(input)) ?? "")
const msgs = yield* MessageV2.toModelMessagesEffect(input.messages, input.model)
return Token.estimate(JSON.stringify(msgs))
})
const select = Effect.fn("SessionCompaction.select")(function* (input: {
@@ -271,10 +254,10 @@ export const layer: Layer.Layer<
model: Provider.Model
}) {
const limit = input.cfg.compaction?.tail_turns ?? DEFAULT_TAIL_TURNS
if (limit <= 0) return { head: input.messages, tail: [] }
if (limit <= 0) return { head: input.messages, tail_start_id: undefined }
const budget = preserveRecentBudget({ cfg: input.cfg, model: input.model })
const all = turns(input.messages)
if (!all.length) return { head: input.messages, tail: [] }
if (!all.length) return { head: input.messages, tail_start_id: undefined }
const recent = all.slice(-limit)
const sizes = yield* Effect.forEach(
recent,
@@ -293,7 +276,7 @@ export const layer: Layer.Layer<
const size = sizes[i]
if (total + size <= budget) {
total += size
keep = { start: turn.start }
keep = { start: turn.start, id: turn.id }
continue
}
const remaining = budget - total
@@ -309,10 +292,10 @@ export const layer: Layer.Layer<
break
}
if (!keep) return { head: input.messages, tail: [] }
if (!keep || keep.start === 0) return { head: input.messages, tail_start_id: undefined }
return {
head: input.messages.slice(0, keep.start),
tail: input.messages.slice(keep.start),
tail_start_id: keep.id,
}
})
@@ -423,10 +406,7 @@ export const layer: Layer.Layer<
{ sessionID: input.sessionID },
{ context: [], prompt: undefined },
)
const tailMessages = structuredClone(selected.tail)
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: tailMessages })
const tail = yield* serialize({ messages: tailMessages, model })
const nextPrompt = compacting.prompt ?? buildPrompt({ previousSummary, context: compacting.context, tail })
const nextPrompt = compacting.prompt ?? buildPrompt({ previousSummary, context: compacting.context })
const msgs = structuredClone(selected.head)
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, {
@@ -493,6 +473,13 @@ export const layer: Layer.Layer<
return "stop"
}
if (compactionPart && selected.tail_start_id && compactionPart.tail_start_id !== selected.tail_start_id) {
yield* session.updatePart({
...compactionPart,
tail_start_id: selected.tail_start_id,
})
}
if (result === "continue" && input.auto) {
if (replay) {
const original = replay.info
@@ -588,6 +575,7 @@ export const layer: Layer.Layer<
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(Date.now()),
text: summary ?? "",
include: selected.tail_start_id,
})
}
yield* bus.publish(Event.Compacted, { sessionID: input.sessionID })
-230
View File
@@ -1,230 +0,0 @@
import { FinishReason, LLMEvent, ProviderMetadata, ToolResultValue } from "@opencode-ai/llm"
import { Effect, Schema } from "effect"
import { type streamText } from "ai"
import { errorMessage } from "@/util/error"
type Result = Awaited<ReturnType<typeof streamText>>
type AISDKEvent = Result["fullStream"] extends AsyncIterable<infer T> ? T : never
export function adapterState() {
return {
step: 0,
text: 0,
reasoning: 0,
currentTextID: undefined as string | undefined,
currentReasoningID: undefined as string | undefined,
toolNames: {} as Record<string, string>,
}
}
function finishReason(value: string | undefined): FinishReason {
return Schema.is(FinishReason)(value) ? value : "unknown"
}
function providerMetadata(value: unknown): ProviderMetadata | undefined {
return Schema.is(ProviderMetadata)(value) ? value : undefined
}
function usage(value: unknown) {
if (!value || typeof value !== "object") return undefined
const item = value as {
inputTokens?: number
outputTokens?: number
totalTokens?: number
reasoningTokens?: number
cachedInputTokens?: number
inputTokenDetails?: { cacheReadTokens?: number; cacheWriteTokens?: number }
outputTokenDetails?: { reasoningTokens?: number }
}
const result = Object.fromEntries(
Object.entries({
inputTokens: item.inputTokens,
outputTokens: item.outputTokens,
totalTokens: item.totalTokens,
reasoningTokens: item.outputTokenDetails?.reasoningTokens ?? item.reasoningTokens,
cacheReadInputTokens: item.inputTokenDetails?.cacheReadTokens ?? item.cachedInputTokens,
cacheWriteInputTokens: item.inputTokenDetails?.cacheWriteTokens,
}).filter((entry) => entry[1] !== undefined),
)
return result
}
function currentTextID(state: ReturnType<typeof adapterState>, id: string | undefined) {
state.currentTextID = id ?? state.currentTextID ?? `text-${state.text++}`
return state.currentTextID
}
function currentReasoningID(state: ReturnType<typeof adapterState>, id: string | undefined) {
state.currentReasoningID = id ?? state.currentReasoningID ?? `reasoning-${state.reasoning++}`
return state.currentReasoningID
}
export function toLLMEvents(
state: ReturnType<typeof adapterState>,
event: AISDKEvent,
): Effect.Effect<ReadonlyArray<LLMEvent>, unknown> {
switch (event.type) {
case "start":
return Effect.succeed([])
case "start-step":
return Effect.succeed([LLMEvent.stepStart({ index: state.step })])
case "finish-step":
return Effect.sync(() => [
LLMEvent.stepFinish({
index: state.step++,
reason: finishReason(event.finishReason),
usage: usage(event.usage),
providerMetadata: providerMetadata(event.providerMetadata),
}),
])
case "finish":
return Effect.sync(() => {
state.toolNames = {}
return [
LLMEvent.requestFinish({
reason: finishReason(event.finishReason),
usage: usage(event.totalUsage),
}),
]
})
case "text-start":
return Effect.sync(() => {
state.currentTextID = currentTextID(state, event.id)
return [
LLMEvent.textStart({
id: state.currentTextID,
providerMetadata: providerMetadata(event.providerMetadata),
}),
]
})
case "text-delta":
return Effect.succeed([
LLMEvent.textDelta({
id: currentTextID(state, event.id),
text: event.text,
}),
])
case "text-end":
return Effect.succeed([
LLMEvent.textEnd({
id: currentTextID(state, event.id),
providerMetadata: providerMetadata(event.providerMetadata),
}),
])
case "reasoning-start":
return Effect.sync(() => {
state.currentReasoningID = currentReasoningID(state, event.id)
return [
LLMEvent.reasoningStart({
id: state.currentReasoningID,
providerMetadata: providerMetadata(event.providerMetadata),
}),
]
})
case "reasoning-delta":
return Effect.succeed([
LLMEvent.reasoningDelta({
id: currentReasoningID(state, event.id),
text: event.text,
}),
])
case "reasoning-end":
return Effect.sync(() => {
const id = currentReasoningID(state, event.id)
state.currentReasoningID = undefined
return [
LLMEvent.reasoningEnd({
id,
providerMetadata: providerMetadata(event.providerMetadata),
}),
]
})
case "tool-input-start":
return Effect.sync(() => {
state.toolNames[event.id] = event.toolName
return [
LLMEvent.toolInputStart({
id: event.id,
name: event.toolName,
providerMetadata: providerMetadata(event.providerMetadata),
}),
]
})
case "tool-input-delta":
return Effect.succeed([
LLMEvent.toolInputDelta({
id: event.id,
name: state.toolNames[event.id] ?? "unknown",
text: event.delta ?? "",
}),
])
case "tool-input-end":
return Effect.succeed([
LLMEvent.toolInputEnd({
id: event.id,
name: state.toolNames[event.id] ?? "unknown",
}),
])
case "tool-call":
return Effect.sync(() => {
state.toolNames[event.toolCallId] = event.toolName
return [
LLMEvent.toolCall({
id: event.toolCallId,
name: event.toolName,
input: event.input,
providerExecuted: "providerExecuted" in event ? event.providerExecuted : undefined,
providerMetadata: providerMetadata(event.providerMetadata),
}),
]
})
case "tool-result":
return Effect.sync(() => {
const name = state.toolNames[event.toolCallId] ?? "unknown"
delete state.toolNames[event.toolCallId]
return [
LLMEvent.toolResult({
id: event.toolCallId,
name,
result: ToolResultValue.make(event.output),
providerExecuted: "providerExecuted" in event ? event.providerExecuted : undefined,
}),
]
})
case "tool-error":
return Effect.sync(() => {
const name = state.toolNames[event.toolCallId] ?? "unknown"
delete state.toolNames[event.toolCallId]
return [
LLMEvent.toolError({
id: event.toolCallId,
name,
message: errorMessage(event.error),
}),
]
})
case "error":
return Effect.fail(event.error)
default:
return Effect.succeed([])
}
}
export * as LLMAISDK from "./llm-ai-sdk"
-190
View File
@@ -1,190 +0,0 @@
import type { JsonSchema, LLMRequest, ProviderMetadata } from "@opencode-ai/llm"
import { LLM, Message, SystemPart, ToolCallPart, ToolDefinition, ToolResultPart } from "@opencode-ai/llm"
import "@opencode-ai/llm/providers"
import type { ModelMessage } from "ai"
import type { Provider } from "@/provider/provider"
type ToolInput = {
readonly description?: string
readonly inputSchema?: unknown
}
export type RequestInput = {
readonly model: Provider.Model
readonly apiKey?: string
readonly baseURL?: string
readonly system?: readonly string[]
readonly messages: readonly ModelMessage[]
readonly tools?: Record<string, ToolInput>
readonly toolChoice?: "auto" | "required" | "none"
readonly temperature?: number
readonly topP?: number
readonly topK?: number
readonly maxOutputTokens?: number
readonly providerOptions?: LLMRequest["providerOptions"]
readonly headers?: Record<string, string>
}
const DEFAULT_BASE_URL: Record<string, string> = {
"@ai-sdk/openai": "https://api.openai.com/v1",
"@ai-sdk/anthropic": "https://api.anthropic.com/v1",
"@ai-sdk/google": "https://generativelanguage.googleapis.com/v1beta",
"@ai-sdk/amazon-bedrock": "https://bedrock-runtime.us-east-1.amazonaws.com",
"@openrouter/ai-sdk-provider": "https://openrouter.ai/api/v1",
}
const ROUTE: Record<string, string> = {
"@ai-sdk/openai": "openai-responses",
"@ai-sdk/azure": "azure-openai-responses",
"@ai-sdk/anthropic": "anthropic-messages",
"@ai-sdk/google": "gemini",
"@ai-sdk/amazon-bedrock": "bedrock-converse",
"@ai-sdk/openai-compatible": "openai-compatible-chat",
"@openrouter/ai-sdk-provider": "openrouter",
}
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value)
const providerMetadata = (value: unknown): ProviderMetadata | undefined => {
if (!isRecord(value)) return undefined
const result = Object.fromEntries(
Object.entries(value).filter((entry): entry is [string, Record<string, unknown>] => isRecord(entry[1])),
)
return Object.keys(result).length === 0 ? undefined : result
}
const textPart = (part: Record<string, unknown>) => ({
type: "text" as const,
text: typeof part.text === "string" ? part.text : "",
providerMetadata: providerMetadata(part.providerOptions),
})
const mediaPart = (part: Record<string, unknown>) => {
if (typeof part.data !== "string" && !(part.data instanceof Uint8Array))
throw new Error("Native LLM request adapter only supports file parts with string or Uint8Array data")
return {
type: "media" as const,
mediaType: typeof part.mediaType === "string" ? part.mediaType : "application/octet-stream",
data: part.data,
filename: typeof part.filename === "string" ? part.filename : undefined,
}
}
const toolResult = (part: Record<string, unknown>) => {
const output = isRecord(part.output) ? part.output : { type: "json", value: part.output }
const type = output.type === "text" ? "text" : output.type === "error-text" ? "error" : "json"
return ToolResultPart.make({
id: typeof part.toolCallId === "string" ? part.toolCallId : "",
name: typeof part.toolName === "string" ? part.toolName : "",
result: "value" in output ? output.value : output,
resultType: type,
providerExecuted: typeof part.providerExecuted === "boolean" ? part.providerExecuted : undefined,
providerMetadata: providerMetadata(part.providerOptions),
})
}
const contentPart = (part: unknown) => {
if (!isRecord(part)) throw new Error("Native LLM request adapter only supports object content parts")
if (part.type === "text") return textPart(part)
if (part.type === "file") return mediaPart(part)
if (part.type === "reasoning")
return {
type: "reasoning" as const,
text: typeof part.text === "string" ? part.text : "",
providerMetadata: providerMetadata(part.providerOptions),
}
if (part.type === "tool-call")
return ToolCallPart.make({
id: typeof part.toolCallId === "string" ? part.toolCallId : "",
name: typeof part.toolName === "string" ? part.toolName : "",
input: part.input,
providerExecuted: typeof part.providerExecuted === "boolean" ? part.providerExecuted : undefined,
providerMetadata: providerMetadata(part.providerOptions),
})
if (part.type === "tool-result") return toolResult(part)
throw new Error(`Native LLM request adapter does not support ${String(part.type)} content parts`)
}
const content = (value: ModelMessage["content"]) =>
typeof value === "string" ? [{ type: "text" as const, text: value }] : value.map(contentPart)
const messages = (input: readonly ModelMessage[]) => {
const system = input.flatMap((message) => (message.role === "system" ? [SystemPart.make(message.content)] : []))
const messages = input.flatMap((message) => {
if (message.role === "system") return []
return [
Message.make({
role: message.role,
content: content(message.content),
native: isRecord(message.providerOptions) ? { providerOptions: message.providerOptions } : undefined,
}),
]
})
return { system, messages }
}
const schema = (value: unknown): JsonSchema => {
if (!isRecord(value)) return { type: "object", properties: {} }
if (isRecord(value.jsonSchema)) return value.jsonSchema
return value
}
const tools = (input: Record<string, ToolInput> | undefined): ToolDefinition[] =>
Object.entries(input ?? {}).map(([name, item]) =>
ToolDefinition.make({
name,
description: item.description ?? "",
inputSchema: schema(item.inputSchema),
}),
)
const generation = (input: RequestInput) => {
const result = {
temperature: input.temperature,
topP: input.topP,
topK: input.topK,
maxTokens: input.maxOutputTokens,
}
return Object.values(result).some((value) => value !== undefined) ? result : undefined
}
const baseURL = (model: Provider.Model) => {
if (model.api.url) return model.api.url
const fallback = DEFAULT_BASE_URL[model.api.npm]
if (fallback) return fallback
throw new Error(`Native LLM request adapter requires a base URL for ${model.providerID}/${model.id}`)
}
export const model = (input: Provider.Model | RequestInput, headers?: Record<string, string>) => {
const model = "model" in input ? input.model : input
const route = ROUTE[model.api.npm]
if (!route) throw new Error(`Native LLM request adapter does not support provider package ${model.api.npm}`)
return LLM.model({
id: model.api.id,
provider: model.providerID,
route,
baseURL: "model" in input && input.baseURL ? input.baseURL : baseURL(model),
apiKey: "model" in input ? input.apiKey : undefined,
headers: Object.keys({ ...model.headers, ...headers }).length === 0 ? undefined : { ...model.headers, ...headers },
limits: {
context: model.limit.context,
output: model.limit.output,
},
})
}
export const request = (input: RequestInput) => {
const converted = messages(input.messages)
return LLM.request({
model: model(input, input.headers),
system: [...(input.system ?? []).map(SystemPart.make), ...converted.system],
messages: converted.messages,
tools: tools(input.tools),
toolChoice: input.toolChoice,
generation: generation(input),
providerOptions: input.providerOptions,
})
}
export * as LLMNative from "./llm-native"
+82 -161
View File
@@ -2,10 +2,7 @@ import { Provider } from "@/provider/provider"
import * as Log from "@opencode-ai/core/util/log"
import { Context, Effect, Layer, Record } from "effect"
import * as Stream from "effect/Stream"
import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool as aiTool, jsonSchema, asSchema } from "ai"
import { tool as nativeTool, ToolFailure, type JsonSchema, type LLMEvent } from "@opencode-ai/llm"
import { LLMClient, RequestExecutor } from "@opencode-ai/llm/route"
import type { LLMClientService } from "@opencode-ai/llm/route"
import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai"
import { mergeDeep } from "remeda"
import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
import { ProviderTransform } from "@/provider/transform"
@@ -19,26 +16,23 @@ import { Flag } from "@opencode-ai/core/flag/flag"
import { Permission } from "@/permission"
import { PermissionID } from "@/permission/schema"
import { Bus } from "@/bus"
import { errorMessage } from "@/util/error"
import { Wildcard } from "@/util/wildcard"
import { SessionID } from "@/session/schema"
import { Auth } from "@/auth"
import { Installation } from "@/installation"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { EffectBridge } from "@/effect/bridge"
import * as Option from "effect/Option"
import * as OtelTracer from "@effect/opentelemetry/Tracer"
import { LLMAISDK } from "./llm-ai-sdk"
import { LLMNative } from "./llm-native"
const log = Log.create({ service: "llm" })
export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX
type Result = Awaited<ReturnType<typeof streamText>>
// Avoid re-instantiating remeda's deep merge types in this hot LLM path; the runtime behavior is still mergeDeep.
const mergeOptions = (target: Record<string, any>, source: Record<string, any> | undefined): Record<string, any> =>
mergeDeep(target, source ?? {}) as Record<string, any>
const runtime = () => (process.env.OPENCODE_LLM_RUNTIME === "native" ? "native" : "ai-sdk")
export type StreamInput = {
user: MessageV2.User
sessionID: string
@@ -58,8 +52,10 @@ export type StreamRequest = StreamInput & {
abort: AbortSignal
}
export type Event = Result["fullStream"] extends AsyncIterable<infer T> ? T : never
export interface Interface {
readonly stream: (input: StreamInput) => Stream.Stream<LLMEvent, unknown>
readonly stream: (input: StreamInput) => Stream.Stream<Event, unknown>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/LLM") {}
@@ -67,7 +63,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/LL
const live: Layer.Layer<
Service,
never,
Auth.Service | Config.Service | Provider.Service | Plugin.Service | Permission.Service | LLMClientService
Auth.Service | Config.Service | Provider.Service | Plugin.Service | Permission.Service
> = Layer.effect(
Service,
Effect.gen(function* () {
@@ -76,7 +72,6 @@ const live: Layer.Layer<
const provider = yield* Provider.Service
const plugin = yield* Plugin.Service
const perm = yield* Permission.Service
const llmClient = yield* LLMClient.Service
const run = Effect.fn("LLM.run")(function* (input: StreamRequest) {
const l = log
@@ -219,7 +214,7 @@ const live: Layer.Layer<
Object.keys(tools).length === 0 &&
hasToolCalls(input.messages)
) {
tools["_noop"] = aiTool({
tools["_noop"] = tool({
description: "Do not call this tool. It exists only for API compatibility and must never be invoked.",
inputSchema: jsonSchema({
type: "object",
@@ -339,120 +334,86 @@ const live: Layer.Layer<
? (yield* InstanceState.context).project.id
: undefined
const requestHeaders = {
...(input.model.providerID.startsWith("opencode")
? {
...(opencodeProjectID ? { "x-opencode-project": opencodeProjectID } : {}),
"x-opencode-session": input.sessionID,
"x-opencode-request": input.user.id,
"x-opencode-client": Flag.OPENCODE_CLIENT,
"User-Agent": `opencode/${InstallationVersion}`,
}
: {
"x-session-affinity": input.sessionID,
...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}),
"User-Agent": `opencode/${InstallationVersion}`,
}),
...input.model.headers,
...headers,
}
if (runtime() === "native") {
if (input.model.providerID !== "openai" || input.model.api.npm !== "@ai-sdk/openai") {
return yield* Effect.fail(new Error("Native LLM runtime currently only supports OpenAI models"))
}
const apiKey =
info?.type === "api" ? info.key : typeof item.options.apiKey === "string" ? item.options.apiKey : undefined
if (!apiKey) return yield* Effect.fail(new Error("Native LLM runtime requires API key auth for OpenAI"))
const baseURL = typeof item.options.baseURL === "string" ? item.options.baseURL : undefined
const request = LLMNative.request({
model: input.model,
apiKey,
baseURL,
system: isOpenaiOauth ? system : [],
messages: ProviderTransform.message(messages, input.model, options),
tools: sortedTools,
toolChoice: input.toolChoice,
temperature: params.temperature,
topP: params.topP,
topK: params.topK,
maxOutputTokens: params.maxOutputTokens,
providerOptions: ProviderTransform.providerOptions(input.model, params.options),
headers: requestHeaders,
})
return {
type: "native" as const,
stream: llmClient.stream({ request, tools: nativeTools(sortedTools, input) }),
}
}
return {
type: "ai-sdk" as const,
result: streamText({
onError(error) {
l.error("stream error", {
error,
return streamText({
onError(error) {
l.error("stream error", {
error,
})
},
async experimental_repairToolCall(failed) {
const lower = failed.toolCall.toolName.toLowerCase()
if (lower !== failed.toolCall.toolName && sortedTools[lower]) {
l.info("repairing tool call", {
tool: failed.toolCall.toolName,
repaired: lower,
})
},
async experimental_repairToolCall(failed) {
const lower = failed.toolCall.toolName.toLowerCase()
if (lower !== failed.toolCall.toolName && sortedTools[lower]) {
l.info("repairing tool call", {
tool: failed.toolCall.toolName,
repaired: lower,
})
return {
...failed.toolCall,
toolName: lower,
}
}
return {
...failed.toolCall,
input: JSON.stringify({
tool: failed.toolCall.toolName,
error: failed.error.message,
}),
toolName: "invalid",
toolName: lower,
}
},
temperature: params.temperature,
topP: params.topP,
topK: params.topK,
providerOptions: ProviderTransform.providerOptions(input.model, params.options),
activeTools: Object.keys(sortedTools).filter((x) => x !== "invalid"),
tools: sortedTools,
toolChoice: input.toolChoice,
maxOutputTokens: params.maxOutputTokens,
abortSignal: input.abort,
headers: requestHeaders,
maxRetries: input.retries ?? 0,
messages,
model: wrapLanguageModel({
model: language,
middleware: [
{
specificationVersion: "v3" as const,
async transformParams(args) {
if (args.type === "stream") {
// @ts-expect-error
args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options)
}
return args.params
},
}
return {
...failed.toolCall,
input: JSON.stringify({
tool: failed.toolCall.toolName,
error: failed.error.message,
}),
toolName: "invalid",
}
},
temperature: params.temperature,
topP: params.topP,
topK: params.topK,
providerOptions: ProviderTransform.providerOptions(input.model, params.options),
activeTools: Object.keys(sortedTools).filter((x) => x !== "invalid"),
tools: sortedTools,
toolChoice: input.toolChoice,
maxOutputTokens: params.maxOutputTokens,
abortSignal: input.abort,
headers: {
...(input.model.providerID.startsWith("opencode")
? {
"x-opencode-project": opencodeProjectID,
"x-opencode-session": input.sessionID,
"x-opencode-request": input.user.id,
"x-opencode-client": Flag.OPENCODE_CLIENT,
"User-Agent": `opencode/${InstallationVersion}`,
}
: {
"x-session-affinity": input.sessionID,
...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}),
"User-Agent": `opencode/${InstallationVersion}`,
}),
...input.model.headers,
...headers,
},
maxRetries: input.retries ?? 0,
messages,
model: wrapLanguageModel({
model: language,
middleware: [
{
specificationVersion: "v3" as const,
async transformParams(args) {
if (args.type === "stream") {
// @ts-expect-error
args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options)
}
return args.params
},
],
}),
experimental_telemetry: {
isEnabled: cfg.experimental?.openTelemetry,
functionId: "session.llm",
tracer: telemetryTracer,
metadata: {
userId: cfg.username ?? "unknown",
sessionId: input.sessionID,
},
},
],
}),
}
experimental_telemetry: {
isEnabled: cfg.experimental?.openTelemetry,
functionId: "session.llm",
tracer: telemetryTracer,
metadata: {
userId: cfg.username ?? "unknown",
sessionId: input.sessionID,
},
},
})
})
const stream: Interface["stream"] = (input) =>
@@ -466,15 +427,7 @@ const live: Layer.Layer<
const result = yield* run({ ...input, abort: ctrl.signal })
if (result.type === "native") return result.stream
const state = LLMAISDK.adapterState()
return Stream.fromAsyncIterable(result.result.fullStream, (e) =>
e instanceof Error ? e : new Error(String(e)),
).pipe(
Stream.mapEffect((event) => LLMAISDK.toLLMEvents(state, event)),
Stream.flatMap((events) => Stream.fromIterable(events)),
)
return Stream.fromAsyncIterable(result.fullStream, (e) => (e instanceof Error ? e : new Error(String(e))))
}),
),
)
@@ -491,7 +444,6 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(Config.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(LLMClient.layer.pipe(Layer.provide(RequestExecutor.defaultLayer))),
),
)
@@ -503,37 +455,6 @@ function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "permission"
return Record.filter(input.tools, (_, k) => input.user.tools?.[k] !== false && !disabled.has(k))
}
function nativeSchema(value: unknown): JsonSchema {
if (!value || typeof value !== "object") return { type: "object", properties: {} }
if ("jsonSchema" in value && value.jsonSchema && typeof value.jsonSchema === "object")
return value.jsonSchema as JsonSchema
return asSchema(value as Parameters<typeof asSchema>[0]).jsonSchema as JsonSchema
}
function nativeTools(tools: Record<string, Tool>, input: StreamRequest) {
return Object.fromEntries(
Object.entries(tools).map(([name, item]) => [
name,
nativeTool({
description: item.description ?? "",
jsonSchema: nativeSchema(item.inputSchema),
execute: (args: unknown, ctx?: { readonly id: string; readonly name: string }) =>
Effect.tryPromise({
try: () => {
if (!item.execute) throw new Error(`Tool has no execute handler: ${name}`)
return item.execute(args, {
toolCallId: ctx?.id ?? name,
messages: input.messages,
abortSignal: input.abort,
})
},
catch: (error) => new ToolFailure({ message: errorMessage(error) }),
}),
}),
]),
)
}
// Check if messages contain any tool-call content
// Used to determine if a dummy tool should be added for LiteLLM proxy compatibility
export function hasToolCalls(messages: ModelMessage[]): boolean {
@@ -0,0 +1,14 @@
import { Schema } from "effect"
import { NamedError } from "@opencode-ai/core/util/error"
export const OutputLengthError = NamedError.create("MessageOutputLengthError", {})
export const AuthError = NamedError.create("ProviderAuthError", {
providerID: Schema.String,
message: Schema.String,
})
export const Shared = [AuthError.EffectSchema, NamedError.Unknown.EffectSchema, OutputLengthError.EffectSchema] as const
export const SharedSchema = Schema.Union(Shared)
export * as MessageError from "./message-error"
+48 -17
View File
@@ -23,8 +23,10 @@ import type { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "@/provider/schema"
import { Effect, Schema, Types } from "effect"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { namedSchemaError } from "@/util/named-schema-error"
import * as EffectLogger from "@opencode-ai/core/effect/logger"
import { MessageError } from "./message-error"
import { AuthError, OutputLengthError } from "./message-error"
export { AuthError, OutputLengthError } from "./message-error"
/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
interface FetchDecompressionError extends Error {
@@ -36,17 +38,12 @@ interface FetchDecompressionError extends Error {
export const SYNTHETIC_ATTACHMENT_PROMPT = "Attached media from tool result:"
export { isMedia }
export const OutputLengthError = namedSchemaError("MessageOutputLengthError", {})
export const AbortedError = namedSchemaError("MessageAbortedError", { message: Schema.String })
export const StructuredOutputError = namedSchemaError("StructuredOutputError", {
export const AbortedError = NamedError.create("MessageAbortedError", { message: Schema.String })
export const StructuredOutputError = NamedError.create("StructuredOutputError", {
message: Schema.String,
retries: NonNegativeInt,
})
export const AuthError = namedSchemaError("ProviderAuthError", {
providerID: Schema.String,
message: Schema.String,
})
export const APIError = namedSchemaError("APIError", {
export const APIError = NamedError.create("APIError", {
message: Schema.String,
statusCode: Schema.optional(NonNegativeInt),
isRetryable: Schema.Boolean,
@@ -55,7 +52,7 @@ export const APIError = namedSchemaError("APIError", {
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
})
export type APIError = Schema.Schema.Type<typeof APIError.Schema>
export const ContextOverflowError = namedSchemaError("ContextOverflowError", {
export const ContextOverflowError = NamedError.create("ContextOverflowError", {
message: Schema.String,
responseBody: Schema.optional(Schema.String),
})
@@ -381,9 +378,7 @@ export type Part =
| CompactionPart
const AssistantErrorSchema = Schema.Union([
AuthError.EffectSchema,
NamedError.Unknown.EffectSchema,
OutputLengthError.EffectSchema,
...MessageError.Shared,
AbortedError.EffectSchema,
StructuredOutputError.EffectSchema,
ContextOverflowError.EffectSchema,
@@ -777,13 +772,12 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
return part.metadata?.anthropic?.signature != null
})
for (const part of msg.parts) {
if (msg.info.summary && part.type !== "text") continue
if (part.type === "text") {
const text = part.text === "" && hasSignedReasoning ? " " : part.text
assistantMessage.parts.push({
type: "text",
text,
...(differentModel || msg.info.summary ? {} : { providerMetadata: part.metadata }),
...(differentModel ? {} : { providerMetadata: part.metadata }),
})
}
if (part.type === "step-start")
@@ -1009,16 +1003,53 @@ export function get(input: { sessionID: SessionID; messageID: MessageID }): With
export function filterCompacted(msgs: Iterable<WithParts>) {
const result = [] as WithParts[]
const completed = new Set<string>()
let retain: MessageID | undefined
for (const msg of msgs) {
result.push(msg)
if (msg.info.role === "user" && completed.has(msg.info.id)) {
if (msg.parts.some((item): item is CompactionPart => item.type === "compaction")) break
if (retain) {
if (msg.info.id === retain) break
continue
}
if (msg.info.role === "user" && completed.has(msg.info.id)) {
const part = msg.parts.find((item): item is CompactionPart => item.type === "compaction")
if (!part) continue
if (!part.tail_start_id) break
retain = part.tail_start_id
if (msg.info.id === retain) break
continue
}
if (msg.info.role === "user" && completed.has(msg.info.id) && msg.parts.some((part) => part.type === "compaction"))
break
if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish && !msg.info.error)
completed.add(msg.info.parentID)
}
result.reverse()
const compactionIndex = result.findLastIndex(
(msg) =>
msg.info.role === "user" &&
msg.parts.some((item): item is CompactionPart => item.type === "compaction" && item.tail_start_id !== undefined),
)
const compaction = result[compactionIndex]
const part = compaction?.parts.find(
(item): item is CompactionPart => item.type === "compaction" && item.tail_start_id !== undefined,
)
const summaryIndex = compaction
? result.findIndex(
(msg, index) =>
index > compactionIndex &&
msg.info.role === "assistant" &&
msg.info.summary &&
msg.info.parentID === compaction.info.id,
)
: -1
const tailIndex = part?.tail_start_id ? result.findIndex((msg) => msg.info.id === part.tail_start_id) : -1
if (tailIndex >= 0 && tailIndex < compactionIndex && summaryIndex > compactionIndex) {
return [
...result.slice(compactionIndex, summaryIndex + 1),
...result.slice(tailIndex, compactionIndex),
...result.slice(summaryIndex + 1),
]
}
return result
}
+4 -11
View File
@@ -2,14 +2,9 @@ import { Schema } from "effect"
import { SessionID } from "./schema"
import { ModelID, ProviderID } from "../provider/schema"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { namedSchemaError } from "@/util/named-schema-error"
import { NamedError } from "@opencode-ai/core/util/error"
export const OutputLengthError = namedSchemaError("MessageOutputLengthError", {})
export const AuthError = namedSchemaError("ProviderAuthError", {
providerID: Schema.String,
message: Schema.String,
})
import { MessageError } from "./message-error"
import { AuthError, OutputLengthError } from "./message-error"
export { AuthError, OutputLengthError } from "./message-error"
export const ToolCall = Schema.Struct({
state: Schema.Literal("call"),
@@ -105,9 +100,7 @@ export const Info = Schema.Struct({
created: NonNegativeInt,
completed: Schema.optional(NonNegativeInt),
}),
error: Schema.optional(
Schema.Union([AuthError.EffectSchema, NamedError.Unknown.EffectSchema, OutputLengthError.EffectSchema]),
),
error: Schema.optional(MessageError.SharedSchema),
sessionID: SessionID,
tool: Schema.Record(
Schema.String,
+118 -200
View File
@@ -1,4 +1,4 @@
import { Cause, Deferred, Effect, Exit, Layer, Context, Scope, Schema } from "effect"
import { Cause, Deferred, Effect, Exit, Layer, Context, Scope } from "effect"
import * as Stream from "effect/Stream"
import { Agent } from "@/agent/agent"
import { Bus } from "@/bus"
@@ -26,13 +26,14 @@ import { SessionEvent } from "@/v2/session-event"
import { Modelv2 } from "@/v2/model"
import * as DateTime from "effect/DateTime"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Usage, type LLMEvent } from "@opencode-ai/llm"
const DOOM_LOOP_THRESHOLD = 3
const log = Log.create({ service: "session.processor" })
export type Result = "compact" | "stop" | "continue"
export type Event = LLM.Event
export interface Handle {
readonly message: MessageV2.Assistant
readonly updateToolCall: (
@@ -66,7 +67,6 @@ type ToolCall = {
messageID: MessageV2.ToolPart["messageID"]
sessionID: MessageV2.ToolPart["sessionID"]
done: Deferred.Deferred<void>
inputEnded: boolean
}
interface ProcessorContext extends Input {
@@ -79,7 +79,7 @@ interface ProcessorContext extends Input {
reasoningMap: Record<string, MessageV2.ReasoningPart>
}
type StreamEvent = LLMEvent
type StreamEvent = Event
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionProcessor") {}
@@ -223,85 +223,9 @@ export const layer: Layer.Layer<
return true
})
const finishReasoning = Effect.fn("SessionProcessor.finishReasoning")(function* (reasoningID: string) {
if (!(reasoningID in ctx.reasoningMap)) return
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Reasoning.Ended.Sync, {
sessionID: ctx.sessionID,
reasoningID,
text: ctx.reasoningMap[reasoningID].text,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
// oxlint-disable-next-line no-self-assign -- reactivity trigger
ctx.reasoningMap[reasoningID].text = ctx.reasoningMap[reasoningID].text
ctx.reasoningMap[reasoningID].time = { ...ctx.reasoningMap[reasoningID].time, end: Date.now() }
yield* session.updatePart(ctx.reasoningMap[reasoningID])
delete ctx.reasoningMap[reasoningID]
})
const ensureToolCall = Effect.fn("SessionProcessor.ensureToolCall")(function* (input: {
id: string
name: string
providerExecuted?: boolean
}) {
const existing = yield* readToolCall(input.id)
if (existing) return existing
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Tool.Input.Started.Sync, {
sessionID: ctx.sessionID,
callID: input.id,
name: input.name,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
const part = yield* session.updatePart({
id: PartID.ascending(),
messageID: ctx.assistantMessage.id,
sessionID: ctx.assistantMessage.sessionID,
type: "tool",
tool: input.name,
callID: input.id,
state: { status: "pending", input: {}, raw: "" },
metadata: input.providerExecuted ? { providerExecuted: true } : undefined,
} satisfies MessageV2.ToolPart)
ctx.toolcalls[input.id] = {
done: yield* Deferred.make<void>(),
partID: part.id,
messageID: part.messageID,
sessionID: part.sessionID,
inputEnded: false,
}
return { call: ctx.toolcalls[input.id], part }
})
const isFilePart = Schema.is(MessageV2.FilePart)
const toolResultOutput = (value: Extract<StreamEvent, { type: "tool-result" }>) => {
if (isRecord(value.result.value) && typeof value.result.value.output === "string") {
return {
title: typeof value.result.value.title === "string" ? value.result.value.title : value.name,
metadata: isRecord(value.result.value.metadata) ? value.result.value.metadata : {},
output: value.result.value.output,
attachments: Array.isArray(value.result.value.attachments)
? value.result.value.attachments.filter(isFilePart)
: undefined,
}
}
return {
title: value.name,
metadata: value.result.type === "json" && isRecord(value.result.value) ? value.result.value : {},
output: typeof value.result.value === "string" ? value.result.value : (JSON.stringify(value.result.value) ?? ""),
}
}
const toolInput = (value: unknown): Record<string, any> => (isRecord(value) ? value : { value })
const handleEvent = Effect.fnUntraced(function* (value: StreamEvent) {
switch (value.type) {
case "request-start":
case "start":
yield* status.set(ctx.sessionID, { type: "busy" })
return
@@ -327,132 +251,116 @@ export const layer: Layer.Layer<
yield* session.updatePart(ctx.reasoningMap[value.id])
return
case "reasoning-delta": {
const reasoningID = value.id ?? "reasoning"
if (!(reasoningID in ctx.reasoningMap)) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Reasoning.Started.Sync, {
sessionID: ctx.sessionID,
reasoningID,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
ctx.reasoningMap[reasoningID] = {
id: PartID.ascending(),
messageID: ctx.assistantMessage.id,
sessionID: ctx.assistantMessage.sessionID,
type: "reasoning",
text: "",
time: { start: Date.now() },
}
yield* session.updatePart(ctx.reasoningMap[reasoningID])
}
ctx.reasoningMap[reasoningID].text += value.text
case "reasoning-delta":
if (!(value.id in ctx.reasoningMap)) return
ctx.reasoningMap[value.id].text += value.text
if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata
yield* session.updatePartDelta({
sessionID: ctx.reasoningMap[reasoningID].sessionID,
messageID: ctx.reasoningMap[reasoningID].messageID,
partID: ctx.reasoningMap[reasoningID].id,
sessionID: ctx.reasoningMap[value.id].sessionID,
messageID: ctx.reasoningMap[value.id].messageID,
partID: ctx.reasoningMap[value.id].id,
field: "text",
delta: value.text,
})
return
}
case "reasoning-end":
if (value.providerMetadata && value.id in ctx.reasoningMap) {
ctx.reasoningMap[value.id].metadata = value.providerMetadata
if (!(value.id in ctx.reasoningMap)) return
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Reasoning.Ended.Sync, {
sessionID: ctx.sessionID,
reasoningID: value.id,
text: ctx.reasoningMap[value.id].text,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
yield* finishReasoning(value.id)
// oxlint-disable-next-line no-self-assign -- reactivity trigger
ctx.reasoningMap[value.id].text = ctx.reasoningMap[value.id].text
ctx.reasoningMap[value.id].time = { ...ctx.reasoningMap[value.id].time, end: Date.now() }
if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata
yield* session.updatePart(ctx.reasoningMap[value.id])
delete ctx.reasoningMap[value.id]
return
case "tool-input-start":
if (ctx.assistantMessage.summary) {
throw new Error(`Tool call not allowed while generating summary: ${value.name}`)
throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`)
}
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Tool.Input.Started.Sync, {
sessionID: ctx.sessionID,
callID: value.id,
name: value.toolName,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
const part = yield* session.updatePart({
id: ctx.toolcalls[value.id]?.partID ?? PartID.ascending(),
messageID: ctx.assistantMessage.id,
sessionID: ctx.assistantMessage.sessionID,
type: "tool",
tool: value.toolName,
callID: value.id,
state: { status: "pending", input: {}, raw: "" },
metadata: value.providerExecuted ? { providerExecuted: true } : undefined,
} satisfies MessageV2.ToolPart)
ctx.toolcalls[value.id] = {
done: yield* Deferred.make<void>(),
partID: part.id,
messageID: part.messageID,
sessionID: part.sessionID,
}
yield* ensureToolCall(value)
return
case "tool-input-delta": {
if (ctx.assistantMessage.summary) {
throw new Error(`Tool call not allowed while generating summary: ${value.name}`)
}
yield* ensureToolCall(value)
if (value.text) {
yield* updateToolCall(value.id, (match) => ({
...match,
state:
match.state.status === "pending"
? { ...match.state, raw: match.state.raw + value.text }
: match.state,
}))
}
case "tool-input-delta":
return
}
case "tool-input-end": {
const toolCall = yield* ensureToolCall(value)
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Tool.Input.Ended.Sync, {
sessionID: ctx.sessionID,
callID: value.id,
text: toolCall.part.state.status === "pending" ? toolCall.part.state.raw : "",
text: "",
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
ctx.toolcalls[value.id] = { ...toolCall.call, inputEnded: true }
return
}
case "tool-call": {
if (ctx.assistantMessage.summary) {
throw new Error(`Tool call not allowed while generating summary: ${value.name}`)
}
const toolCall = yield* ensureToolCall(value)
const input = toolInput(value.input)
const raw = toolCall.part.state.status === "pending" ? toolCall.part.state.raw : ""
if (!toolCall.call.inputEnded) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Tool.Input.Ended.Sync, {
sessionID: ctx.sessionID,
callID: value.id,
text: raw,
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`)
}
const toolCall = yield* readToolCall(value.toolCallId)
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Tool.Called.Sync, {
sessionID: ctx.sessionID,
callID: value.id,
tool: value.name,
input,
callID: value.toolCallId,
tool: value.toolName,
input: value.input,
provider: {
executed: toolCall.part.metadata?.providerExecuted === true,
executed: toolCall?.part.metadata?.providerExecuted === true,
...(value.providerMetadata ? { metadata: value.providerMetadata } : {}),
},
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
yield* updateToolCall(value.id, (match) => ({
yield* updateToolCall(value.toolCallId, (match) => ({
...match,
tool: value.name,
state:
match.state.status === "running"
? { ...match.state, input }
: {
status: "running",
input,
time: { start: Date.now() },
},
metadata: {
...match.metadata,
...value.providerMetadata,
...(match.metadata?.providerExecuted ? { providerExecuted: true } : {}),
tool: value.toolName,
state: {
...match.state,
status: "running",
input: value.input,
time: { start: Date.now() },
},
metadata: match.metadata?.providerExecuted
? { ...value.providerMetadata, providerExecuted: true }
: value.providerMetadata,
}))
const parts = MessageV2.parts(ctx.assistantMessage.id)
@@ -463,9 +371,9 @@ export const layer: Layer.Layer<
!recentParts.every(
(part) =>
part.type === "tool" &&
part.tool === value.name &&
part.tool === value.toolName &&
part.state.status !== "pending" &&
JSON.stringify(part.state.input) === JSON.stringify(input),
JSON.stringify(part.state.input) === JSON.stringify(value.input),
)
) {
return
@@ -474,19 +382,27 @@ export const layer: Layer.Layer<
const agent = yield* agents.get(ctx.assistantMessage.agent)
yield* permission.ask({
permission: "doom_loop",
patterns: [value.name],
patterns: [value.toolName],
sessionID: ctx.assistantMessage.sessionID,
metadata: { tool: value.name, input },
always: [value.name],
metadata: { tool: value.toolName, input: value.input },
always: [value.toolName],
ruleset: agent.permission,
})
return
}
case "tool-result": {
const toolCall = yield* readToolCall(value.id)
const rawOutput = toolResultOutput(value)
const normalized = yield* Effect.forEach(rawOutput.attachments ?? [], (attachment) =>
const toolCall = yield* readToolCall(value.toolCallId)
const toolAttachments: MessageV2.FilePart[] = (
Array.isArray(value.output.attachments) ? value.output.attachments : []
).filter(
(attachment: unknown): attachment is MessageV2.FilePart =>
isRecord(attachment) &&
attachment.type === "file" &&
typeof attachment.mime === "string" &&
typeof attachment.url === "string",
)
const normalized = yield* Effect.forEach(toolAttachments, (attachment) =>
attachment.mime.startsWith("image/")
? image.normalize(attachment).pipe(Effect.exit)
: Effect.succeed(Exit.succeed<MessageV2.FilePart>(attachment)),
@@ -494,18 +410,18 @@ export const layer: Layer.Layer<
const omitted = normalized.filter(Exit.isFailure).length
const attachments = normalized.filter(Exit.isSuccess).map((item) => item.value)
const output = {
...rawOutput,
...value.output,
output:
omitted === 0
? rawOutput.output
: `${rawOutput.output}\n\n[${omitted} image${omitted === 1 ? "" : "s"} omitted: could not be resized below the inline image size limit.]`,
attachments: attachments.length ? attachments : undefined,
? value.output.output
: `${value.output.output}\n\n[${omitted} image${omitted === 1 ? "" : "s"} omitted: could not be resized below the inline image size limit.]`,
attachments: attachments?.length ? attachments : undefined,
}
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Tool.Success.Sync, {
sessionID: ctx.sessionID,
callID: value.id,
callID: value.toolCallId,
structured: output.metadata,
content: [
{
@@ -513,32 +429,32 @@ export const layer: Layer.Layer<
text: output.output,
},
...(output.attachments?.map((item: MessageV2.FilePart) => ({
type: "file" as const,
type: "file",
uri: item.url,
mime: item.mime,
name: item.filename,
})) ?? []),
],
provider: {
executed: value.providerExecuted === true || toolCall?.part.metadata?.providerExecuted === true,
executed: toolCall?.part.metadata?.providerExecuted === true,
},
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
yield* completeToolCall(value.id, output)
yield* completeToolCall(value.toolCallId, output)
return
}
case "tool-error": {
const toolCall = yield* readToolCall(value.id)
const toolCall = yield* readToolCall(value.toolCallId)
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Tool.Failed.Sync, {
sessionID: ctx.sessionID,
callID: value.id,
callID: value.toolCallId,
error: {
type: "unknown",
message: value.message,
message: errorMessage(value.error),
},
provider: {
executed: toolCall?.part.metadata?.providerExecuted === true,
@@ -546,14 +462,14 @@ export const layer: Layer.Layer<
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
yield* failToolCall(value.id, new Error(value.message))
yield* failToolCall(value.toolCallId, value.error)
return
}
case "provider-error":
throw new Error(value.message)
case "error":
throw value.error
case "step-start":
case "start-step":
if (!ctx.snapshot) ctx.snapshot = yield* snapshot.track()
if (!ctx.assistantMessage.summary) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
@@ -580,20 +496,19 @@ export const layer: Layer.Layer<
})
return
case "step-finish": {
case "finish-step": {
const completedSnapshot = yield* snapshot.track()
yield* Effect.forEach(Object.keys(ctx.reasoningMap), finishReasoning)
const usage = Session.getUsage({
model: ctx.model,
usage: value.usage ?? new Usage({}),
metadata: value.providerMetadata,
})
const usage = Session.getUsage({
model: ctx.model,
usage: value.usage,
metadata: value.providerMetadata,
})
if (!ctx.assistantMessage.summary) {
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM) {
yield* sync.run(SessionEvent.Step.Ended.Sync, {
sessionID: ctx.sessionID,
finish: value.reason,
finish: value.finishReason,
cost: usage.cost,
tokens: usage.tokens,
snapshot: completedSnapshot,
@@ -601,12 +516,12 @@ export const layer: Layer.Layer<
})
}
}
ctx.assistantMessage.finish = value.reason
ctx.assistantMessage.finish = value.finishReason
ctx.assistantMessage.cost += usage.cost
ctx.assistantMessage.tokens = usage.tokens
yield* session.updatePart({
id: PartID.ascending(),
reason: value.reason,
reason: value.finishReason,
snapshot: completedSnapshot,
messageID: ctx.assistantMessage.id,
sessionID: ctx.assistantMessage.sessionID,
@@ -669,6 +584,7 @@ export const layer: Layer.Layer<
case "text-delta":
if (!ctx.currentText) return
ctx.currentText.text += value.text
if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata
yield* session.updatePartDelta({
sessionID: ctx.currentText.sessionID,
messageID: ctx.currentText.messageID,
@@ -710,9 +626,12 @@ export const layer: Layer.Layer<
ctx.currentText = undefined
return
case "request-finish":
case "finish":
return
default:
slog.info("unhandled", { event: value.type, value })
return
}
})
@@ -814,7 +733,6 @@ export const layer: Layer.Layer<
yield* Effect.gen(function* () {
ctx.currentText = undefined
ctx.reasoningMap = {}
yield* status.set(ctx.sessionID, { type: "busy" })
const stream = llm.stream(streamInput)
yield* stream.pipe(
@@ -898,12 +816,12 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(LLM.defaultLayer),
Layer.provide(Permission.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(Image.defaultLayer),
Layer.provide(SessionSummary.defaultLayer),
Layer.provide(SessionStatus.defaultLayer),
Layer.provide(SyncEvent.defaultLayer),
Layer.provide(Image.defaultLayer),
Layer.provide(Bus.layer),
Layer.provide(Config.defaultLayer),
Layer.provide(SyncEvent.defaultLayer),
),
)
+2 -1
View File
@@ -38,6 +38,7 @@ function applyUsage(db: TxOrDb, sessionID: Session.Info["id"], value: Usage, sig
tokens_reasoning: sql`${SessionTable.tokens_reasoning} + ${value.tokens.reasoning * sign}`,
tokens_cache_read: sql`${SessionTable.tokens_cache_read} + ${value.tokens.cache.read * sign}`,
tokens_cache_write: sql`${SessionTable.tokens_cache_write} + ${value.tokens.cache.write * sign}`,
time_updated: sql`${SessionTable.time_updated}`,
})
.where(eq(SessionTable.id, sessionID))
.run()
@@ -110,7 +111,7 @@ export default [
const info = data.info
const row = db
.update(SessionTable)
.set(toPartialRow(info as Session.Patch))
.set({ time_updated: sql`${SessionTable.time_updated}`, ...toPartialRow(info as Session.Patch) })
.where(eq(SessionTable.id, data.sessionID))
.returning()
.get()
+6 -7
View File
@@ -60,7 +60,6 @@ import * as DateTime from "effect/DateTime"
import { eq } from "@/storage/db"
import * as Database from "@/storage/db"
import { SessionTable } from "./session.sql"
import { LLMEvent } from "@opencode-ai/llm"
// @ts-ignore
globalThis.AI_SDK_LOG_WARNINGS = false
@@ -360,7 +359,7 @@ export const layer = Layer.effect(
messages: [{ role: "user", content: "Generate a title for this conversation:\n" }, ...msgs],
})
.pipe(
Stream.filter(LLMEvent.is.textDelta),
Stream.filter((e): e is Extract<LLM.Event, { type: "text-delta" }> => e.type === "text-delta"),
Stream.map((e) => e.text),
Stream.mkString,
Effect.orDie,
@@ -1084,8 +1083,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the
})
const createUserMessage = Effect.fn("SessionPrompt.createUserMessage")(function* (input: PromptInput) {
const agentName = input.agent || (yield* agents.defaultAgent())
const ag = yield* agents.get(agentName)
const agentName = input.agent
const ag = agentName ? yield* agents.get(agentName) : yield* agents.defaultInfo()
if (!ag) {
const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
@@ -1876,7 +1875,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
throw error
}
const agentName = cmd.agent ?? input.agent ?? (yield* agents.defaultAgent())
const agentName = cmd.agent ?? input.agent
const raw = input.arguments.match(argsRegex) ?? []
const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))
@@ -1929,7 +1928,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
yield* getModel(taskModel.providerID, taskModel.modelID, input.sessionID)
const agent = yield* agents.get(agentName)
const agent = agentName ? yield* agents.get(agentName) : yield* agents.defaultInfo()
if (!agent) {
const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
@@ -1953,7 +1952,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
]
: [...templateParts, ...(input.parts ?? [])]
const userAgent = isSubtask ? (input.agent ?? (yield* agents.defaultAgent())) : agentName
const userAgent = isSubtask ? (input.agent ?? (yield* agents.defaultInfo()).name) : agent.name
const userModel = isSubtask
? input.model
? Provider.parseModel(input.model)
+13 -7
View File
@@ -3,7 +3,7 @@ import path from "path"
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { Decimal } from "decimal.js"
import type { ProviderMetadata, Usage } from "@opencode-ai/llm"
import { type ProviderMetadata, type LanguageModelUsage } from "ai"
import { Flag } from "@opencode-ai/core/flag/flag"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
@@ -373,19 +373,21 @@ export function plan(input: { slug: string; time: { created: number } }, instanc
return path.join(base, [input.time.created, input.slug].join("-") + ".md")
}
export const getUsage = (input: { model: Provider.Model; usage: Usage; metadata?: ProviderMetadata }) => {
export const getUsage = (input: { model: Provider.Model; usage: LanguageModelUsage; metadata?: ProviderMetadata }) => {
const safe = (value: number) => {
if (!Number.isFinite(value)) return 0
return Math.max(0, value)
}
const inputTokens = safe(input.usage.inputTokens ?? 0)
const outputTokens = safe(input.usage.outputTokens ?? 0)
const reasoningTokens = safe(input.usage.reasoningTokens ?? 0)
const reasoningTokens = safe(input.usage.outputTokenDetails?.reasoningTokens ?? input.usage.reasoningTokens ?? 0)
const cacheReadInputTokens = safe(input.usage.cacheReadInputTokens ?? 0)
const cacheReadInputTokens = safe(
input.usage.inputTokenDetails?.cacheReadTokens ?? input.usage.cachedInputTokens ?? 0,
)
const cacheWriteInputTokens = safe(
Number(
input.usage.cacheWriteInputTokens ??
input.usage.inputTokenDetails?.cacheWriteTokens ??
input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
// google-vertex-anthropic returns metadata under "vertex" key
// (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages')
@@ -416,10 +418,14 @@ export const getUsage = (input: { model: Provider.Model; usage: Usage; metadata?
},
}
const contextTokens = inputTokens
const costInfo =
input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000
input.model.cost?.tiers
?.filter((item) => item.tier.type === "context" && contextTokens > item.tier.size)
.sort((a, b) => b.tier.size - a.tier.size)[0] ??
(input.model.cost?.experimentalOver200K && contextTokens > 200_000
? input.model.cost.experimentalOver200K
: input.model.cost
: input.model.cost)
return {
cost: safe(
new Decimal(0)
+13 -12
View File
@@ -24,7 +24,6 @@ import { ProviderID, type ModelID } from "../provider/schema"
import { WebSearchTool } from "./websearch"
import { RepoCloneTool } from "./repo_clone"
import { RepoOverviewTool } from "./repo_overview"
import { Flag } from "@opencode-ai/core/flag/flag"
import * as Log from "@opencode-ai/core/util/log"
import { LspTool } from "./lsp"
import * as Truncate from "./truncate"
@@ -50,13 +49,11 @@ import { Git } from "@/git"
import { Skill } from "../skill"
import { Permission } from "@/permission"
import { Reference } from "@/reference/reference"
import { RuntimeFlags } from "@/effect/runtime-flags"
const log = Log.create({ service: "tool.registry" })
export function webSearchEnabled(
providerID: ProviderID,
flags = { exa: Flag.OPENCODE_ENABLE_EXA, parallel: Flag.OPENCODE_ENABLE_PARALLEL },
) {
export function webSearchEnabled(providerID: ProviderID, flags = { exa: false, parallel: false }) {
return providerID === ProviderID.opencode || flags.exa || flags.parallel
}
@@ -101,6 +98,7 @@ export const layer: Layer.Layer<
| Ripgrep.Service
| Format.Service
| Truncate.Service
| RuntimeFlags.Service
> = Layer.effect(
Service,
Effect.gen(function* () {
@@ -109,6 +107,7 @@ export const layer: Layer.Layer<
const agents = yield* Agent.Service
const skill = yield* Skill.Service
const truncate = yield* Truncate.Service
const flags = yield* RuntimeFlags.Service
const invalid = yield* InvalidTool
const task = yield* TaskTool
@@ -160,11 +159,13 @@ export const layer: Layer.Layer<
const result = yield* Effect.promise(() => def.execute(args as any, pluginCtx))
const output = typeof result === "string" ? result : result.output
const metadata = typeof result === "string" ? {} : (result.metadata ?? {})
const attachments = typeof result === "string" ? undefined : result.attachments
const info = yield* agent.get(toolCtx.agent)
const out = yield* truncate.output(output, {}, info)
return {
title: "",
title: typeof result === "string" ? "" : (result.title ?? ""),
output: out.truncated ? out.content : output,
attachments,
metadata: {
...metadata,
truncated: out.truncated,
@@ -207,8 +208,7 @@ export const layer: Layer.Layer<
}
yield* config.get()
const questionEnabled =
["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL
const questionEnabled = ["app", "cli", "desktop"].includes(flags.client) || flags.enableQuestionTool
const tool = yield* Effect.all({
invalid: Tool.init(invalid),
@@ -246,11 +246,11 @@ export const layer: Layer.Layer<
tool.fetch,
tool.todo,
tool.search,
...(Flag.OPENCODE_EXPERIMENTAL_SCOUT ? [tool.repo_clone, tool.repo_overview] : []),
...(flags.experimentalScout ? [tool.repo_clone, tool.repo_overview] : []),
tool.skill,
tool.patch,
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [tool.lsp] : []),
...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [tool.plan] : []),
...(flags.experimentalLspTool ? [tool.lsp] : []),
...(flags.experimentalPlanMode && flags.client === "cli" ? [tool.plan] : []),
],
task: tool.task,
read: tool.read,
@@ -304,7 +304,7 @@ export const layer: Layer.Layer<
const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) {
const filtered = (yield* all()).filter((tool) => {
if (tool.id === WebSearchTool.id) {
return webSearchEnabled(input.providerID)
return webSearchEnabled(input.providerID, { exa: flags.enableExa, parallel: flags.enableParallel })
}
const usePatch =
@@ -378,6 +378,7 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(Truncate.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
),
)
+7 -6
View File
@@ -3,9 +3,9 @@ import { HttpClient } from "effect/unstable/http"
import * as Tool from "./tool"
import * as McpWebSearch from "./mcp-websearch"
import DESCRIPTION from "./websearch.txt"
import { Flag } from "@opencode-ai/core/flag/flag"
import { checksum } from "@opencode-ai/core/util/encode"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { RuntimeFlags } from "@/effect/runtime-flags"
export const Parameters = Schema.Struct({
query: Schema.String.annotate({ description: "Websearch query" }),
@@ -27,10 +27,7 @@ export const Parameters = Schema.Struct({
const WebSearchProviderSchema = Schema.Literals(["exa", "parallel"])
export type WebSearchProvider = Schema.Schema.Type<typeof WebSearchProviderSchema>
export function selectWebSearchProvider(
sessionID: string,
flags = { exa: Flag.OPENCODE_ENABLE_EXA, parallel: Flag.OPENCODE_ENABLE_PARALLEL },
): WebSearchProvider {
export function selectWebSearchProvider(sessionID: string, flags = { exa: false, parallel: false }): WebSearchProvider {
const override = process.env.OPENCODE_WEBSEARCH_PROVIDER
if (override === "exa" || override === "parallel") return override
if (flags.parallel) return "parallel"
@@ -103,6 +100,7 @@ export const WebSearchTool = Tool.define(
"websearch",
Effect.gen(function* () {
const http = yield* HttpClient.HttpClient
const flags = yield* RuntimeFlags.Service
return {
get description() {
@@ -111,7 +109,10 @@ export const WebSearchTool = Tool.define(
parameters: Parameters,
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
Effect.gen(function* () {
const provider = selectWebSearchProvider(ctx.sessionID)
const provider = selectWebSearchProvider(ctx.sessionID, {
exa: flags.enableExa,
parallel: flags.enableParallel,
})
const title = webSearchProviderLabel(provider)
yield* ctx.metadata({ title: `${title} "${params.query}"`, metadata: { provider } })
+8 -1
View File
@@ -1,10 +1,11 @@
import { chmod, mkdir, readFile, stat as statFile, writeFile } from "fs/promises"
import { createWriteStream, existsSync, statSync } from "fs"
import { realpathSync } from "fs"
import { dirname, join, relative, resolve as pathResolve, win32 } from "path"
import { dirname, isAbsolute, join, relative, resolve as pathResolve, win32 } from "path"
import { Readable } from "stream"
import { pipeline } from "stream/promises"
import { Glob } from "@opencode-ai/core/util/glob"
import { fileURLToPath } from "url"
// Fast sync version for metadata checks
export async function exists(p: string): Promise<boolean> {
@@ -142,6 +143,12 @@ export function resolve(p: string): string {
}
}
export function resolveFilePath(root: string, file: string): string {
const raw = file.startsWith("file://") ? fileURLToPath(file) : file
if (isAbsolute(raw)) return raw
return pathResolve(root, raw)
}
export function windowsPath(p: string): string {
if (process.platform !== "win32") return p
return (
@@ -1,9 +0,0 @@
import { Schema } from "effect"
import { NamedError } from "@opencode-ai/core/util/error"
/**
* Create a Schema-backed NamedError-shaped class.
*/
export function namedSchemaError<Tag extends string, Fields extends Schema.Struct.Fields>(tag: Tag, fields: Fields) {
return NamedError.create(tag, fields)
}

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