From e8b8bc4bbf448d2b4a8785cfce830e0a218eb8e3 Mon Sep 17 00:00:00 2001
From: Franco Carballar
Date: Wed, 30 Sep 2026 17:08:39 -0300
Subject: [PATCH 1/6] fix!: preserve voice model selection and exclude PipeWire
call audio from ducking
---
README.md | 4 +-
bun.lock | 252 +++++++-----------------------------------
native/src/duck.rs | 68 +++++++++++-
package.json | 6 +-
src/tui/controller.ts | 2 +
src/tui/model.ts | 17 +++
test/model.test.ts | 45 ++++++++
7 files changed, 174 insertions(+), 220 deletions(-)
create mode 100644 src/tui/model.ts
create mode 100644 test/model.test.ts
diff --git a/README.md b/README.md
index b24cedc..6c60692 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
-
+
@@ -51,7 +51,7 @@ throughout the call.
## Requirements
-- [OpenCode](https://opencode.ai) 2.0.14 or newer.
+- [OpenCode](https://opencode.ai) 2.0.20 or newer.
- A ChatGPT account signed in to OpenCode: run `/connect`, choose OpenAI, then "ChatGPT Pro/Plus". Voice availability
and usage limits depend on your ChatGPT plan.
- A microphone and speakers (or headphones).
diff --git a/bun.lock b/bun.lock
index 60695f7..2696aed 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,11 +5,11 @@
"": {
"name": "opencode-gpt-live",
"dependencies": {
- "@opencode/plugin": "^2.0.14",
+ "@opencode/plugin": "^2.0.20",
"zod": "4.1.8",
},
"devDependencies": {
- "@opencode/client": "2.0.14",
+ "@opencode/client": "2.0.20",
"@opencode/theme": "2.0.14",
"@opentui/core": "0.5.12",
"@types/bun": "latest",
@@ -25,8 +25,6 @@
"packages": {
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
- "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
-
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
@@ -75,62 +73,6 @@
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="],
- "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
-
- "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
-
- "@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=="],
-
- "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
-
- "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="],
-
- "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
-
- "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="],
-
- "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
-
- "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="],
-
- "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
-
- "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
-
- "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="],
-
- "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
-
- "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="],
-
- "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="],
-
- "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
-
- "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
-
- "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
-
- "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
-
- "@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="],
-
- "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="],
-
- "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="],
-
- "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="],
-
- "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-FFwIwzU+7SCOuxxV4YtJql6T9981ZVTm+FHO5GhVsRqCTdy0WwrEZh3l42ARpXruJUDGOptdduHW6Zr7pNPLNg=="],
-
- "@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="],
-
- "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
-
- "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
-
- "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
-
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-rc.112", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0-rc.112" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-OTRv1DxTHUmnakgJ6XVM8wVgF1KgZH4UXnOemwSLUwUjXO+RCikzF8oR/rlVmOGq81KtQzj1URM4M4nchlQOuQ=="],
"@effect/platform-node": ["@effect/platform-node@4.0.0-rc.112", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.112", "mime": "^4.1.0", "undici": "^8.10.0" }, "peerDependencies": { "effect": "^4.0.0-rc.112", "redis": ">=5.0.0 <7.0.0" } }, "sha512-/BMAcdNGQQskLmI0Zoa95KfTZkr9HV9N4NSxaSrusG6GeW6Ulp9KvZ+Rlaiw8lnOt43CXjFLdfll5/k5rxL4hQ=="],
@@ -145,14 +87,6 @@
"@isaacs/string-locale-compare": ["@isaacs/string-locale-compare@1.1.0", "", {}, "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ=="],
- "@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=="],
-
- "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
-
- "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
-
- "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
-
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="],
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="],
@@ -195,43 +129,43 @@
"@npmcli/run-script": ["@npmcli/run-script@10.0.4", "", { "dependencies": { "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", "proc-log": "^6.0.0" } }, "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg=="],
- "@opencode/ai": ["@opencode/ai@2.0.14", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "2.0.14", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.0.0-rc.112", "google-auth-library": "10.5.0" } }, "sha512-8TayJbwQyzKI+NQF/7/v0JdbVSRvdbiZRjJ3RTpV3Tbp0YwlL9M6YxtV5fPvLkOplB4b/hCiZkJ77enJN1tZeA=="],
+ "@opencode/ai": ["@opencode/ai@2.0.20", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "2.0.20", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.0.0-rc.112", "google-auth-library": "10.5.0" } }, "sha512-OgLbrorEgk38TCZIzoMkcUbe2DiARYPWD4Kp5pInlrI/s1aAAm38GydYDYEJHgkCd5FBerv1dKWGefpyMjTUMg=="],
- "@opencode/client": ["@opencode/client@2.0.14", "", { "dependencies": { "@opencode/protocol": "2.0.14", "@opencode/schema": "2.0.14" }, "peerDependencies": { "effect": "4.0.0-rc.112", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-9RQGQM8cfxz9/qBUfCp+l2KQoVh418+XNpYHAMMTtstVRQjvQN2r5FO1Rc4U4tM1zAbtaSZZ4vGgE4Pi+tGlJQ=="],
+ "@opencode/client": ["@opencode/client@2.0.20", "", { "dependencies": { "@opencode/protocol": "2.0.20", "@opencode/schema": "2.0.20" }, "peerDependencies": { "effect": "4.0.0-rc.112", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-gTwZCVv7tNwOLPZXZyUDF9tZnEsKuchkjXv1l98I+sTWJJNRqsEM5wnvjlOJEiwiqWZTeBk1UIk2QpR5PDGzwA=="],
- "@opencode/plugin": ["@opencode/plugin@2.0.14", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "2.0.14", "@opencode/client": "2.0.14", "@opencode/protocol": "2.0.14", "@opencode/schema": "2.0.14", "@opencode/util": "2.0.14", "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112", "zod": "4.1.8" }, "peerDependencies": { "@opencode/theme": "2.0.14", "@opentui/core": ">=0.5.10", "@opentui/solid": ">=0.5.10", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-TRW+lMZFq93Kv+XLjpIsFHHNO88ynkPeuXUrDlNJw6u0wN5hiBUXlR4PWVE/7LKSdQegoIwGuVCAhPsGxEhIQA=="],
+ "@opencode/plugin": ["@opencode/plugin@2.0.20", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "2.0.20", "@opencode/client": "2.0.20", "@opencode/protocol": "2.0.20", "@opencode/schema": "2.0.20", "@opencode/util": "2.0.20", "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112", "zod": "4.1.8" }, "peerDependencies": { "@opencode/theme": "2.0.20", "@opentui/core": ">=0.5.12", "@opentui/solid": ">=0.5.12", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-RMJQH3cgOw7hztUUTnnv1WJ3veKYV2oxkasfEYEdzVIVvIK3LYwYZqrnlZS4VLRNOkyCeypfPDKrt0iYNPmPUw=="],
- "@opencode/protocol": ["@opencode/protocol@2.0.14", "", { "dependencies": { "@opencode/schema": "2.0.14", "effect": "4.0.0-rc.112" } }, "sha512-erI+XbxKSR3/IBnqDzJ8SU8j/qc684mfVmn0HQvPgNSKFEeCLI2x40U47SBZFAhoXeJl4EsOZveUhZfiOIpDuw=="],
+ "@opencode/protocol": ["@opencode/protocol@2.0.20", "", { "dependencies": { "@opencode/schema": "2.0.20", "effect": "4.0.0-rc.112" } }, "sha512-5tN25fwpc/2blCFpZgl9fkU92diHAVuk88TGxHH1LO27Ib/lJFWBxTvMUImrCmbyBv70f8W8MMkjkFhA5ryWVA=="],
- "@opencode/schema": ["@opencode/schema@2.0.14", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112" } }, "sha512-iOCtHO05361ybsx0+ETRPYEbBqgDuNhVvbuHM1p1tfE95ZrdxK2imDpA3wEHsNAIibD2mBj8+Sjjs8msJ5Zy6g=="],
+ "@opencode/schema": ["@opencode/schema@2.0.20", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112" } }, "sha512-rutVd9rYHa/WTZA3glLQY3pbMN/SZ6Q4yd8XrpRtbGN+2u3fukz4mTXy8QQY1mVSabdX5jYTmzPi0QJMpmGSUQ=="],
"@opencode/theme": ["@opencode/theme@2.0.14", "", { "dependencies": { "@opentui/core": "0.5.10", "effect": "4.0.0-rc.112" } }, "sha512-EyuGvK0y5B3yLX3FA3i3D+OMEoRHthXIQL08oGtKmgC5kk9hSprCF+RKheTuzTisjBtVMSIhgxfCeFQOx479PQ=="],
- "@opencode/util": ["@opencode/util@2.0.14", "", { "dependencies": { "@effect/opentelemetry": "4.0.0-rc.112", "@effect/platform-node": "4.0.0-rc.112", "@effect/platform-node-shared": "4.0.0-rc.112", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", "cross-spawn": "7.0.6", "effect": "4.0.0-rc.112", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "pacote": "21.5.1" } }, "sha512-AJx5A+IlPZt58ZM74DOp6x/BJUG57V1qs1/be3lvgjuYRL2evx/sMHXHOrr8IDsQBDO6+ZEYxcE5A2mXoHLSBA=="],
+ "@opencode/util": ["@opencode/util@2.0.20", "", { "dependencies": { "@effect/opentelemetry": "4.0.0-rc.112", "@effect/platform-node": "4.0.0-rc.112", "@effect/platform-node-shared": "4.0.0-rc.112", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/exporter-trace-otlp-http": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0", "@opentelemetry/sdk-trace-node": "2.8.0", "cross-spawn": "7.0.6", "effect": "4.0.0-rc.112", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "open": "11.0.4", "pacote": "21.5.1" } }, "sha512-+Vjkjaz7EXXp5AkIVu1ETC4TcxTa+jLtdnzCKVZQf3OuH0Z0MllbcaYaJj/ks1m8T4F7OemITM5bFWr7vBllcQ=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
- "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="],
+ "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg=="],
- "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="],
+ "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.8.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw=="],
- "@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
+ "@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="],
- "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw=="],
+ "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw=="],
- "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="],
+ "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-transformer": "0.219.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA=="],
- "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="],
+ "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="],
- "@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
+ "@opentelemetry/resources": ["@opentelemetry/resources@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg=="],
- "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="],
+ "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA=="],
- "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="],
+ "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg=="],
- "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="],
+ "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ=="],
- "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="],
+ "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.8.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg=="],
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
@@ -253,8 +187,6 @@
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.12", "", { "os": "win32", "cpu": "x64" }, "sha512-KTwtwpfd2zF9opVh3SyRJYDd1o3Xv4XL8OZb8Zi+CqWUel6Y2IDCiVivCv8fGJt3J7wOIXXtuZI9ZUkLyKJCiQ=="],
- "@opentui/solid": ["@opentui/solid@0.5.12", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.12", "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-hAiVlVMtT7AkHGblKwcW1YAuXtxkSy1XSf/RRc4j3IlG3mTNX0bhJdnGOo3Xw14EqeZMp41Mcp5WzHAzMm/DzA=="],
-
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.70.0", "", { "os": "android", "cpu": "arm" }, "sha512-Xd7YO4/T2axEj6FTLcj4Why3mTBqFMg+x24xtorT4Lb2+1g82090GH0a/4U1m0pABGYiix2bq1pqkYrmV3f0Sw=="],
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.70.0", "", { "os": "android", "cpu": "arm64" }, "sha512-x9rlMYyKXdgKdYyUJzGsK1ZV8P4di/J32ipzcS6Jet6p9r9UAh28neXIMtdlSaJJycdi61Z4YkcLKLpk8ueFjg=="],
@@ -333,24 +265,6 @@
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
- "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
-
- "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
-
- "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="],
-
- "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="],
-
- "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="],
-
- "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
-
- "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
-
- "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
-
- "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="],
-
"@redis/bloom": ["@redis/bloom@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-huQgNLaCIZfQ9SeLn4q9124uOUd8HbZDYHwwUzNcRgHqCHiHKl2dDxMqJCeWh8cMqZAoWuHR8XnWbDMIf+o7ag=="],
"@redis/client": ["@redis/client@6.2.1", "", { "dependencies": { "cluster-key-slot": "1.1.2" }, "peerDependencies": { "@node-rs/xxhash": "^1.1.0", "@opentelemetry/api": ">=1 <2" }, "optionalPeers": ["@node-rs/xxhash", "@opentelemetry/api"] }, "sha512-LzxBY7SIBvvJiyCgcaJZZakE3fJrZZ++i24+EDW9fKpCl68D35uJcKFpZZwCfOoG9WZTbyZlMzMeM0gtOAMU9Q=="],
@@ -417,18 +331,10 @@
"aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
- "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.10", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-lxve6Y02YiZTldB7efKpnbf1BH00XCFZNYYW235jSGsYaJNFtHrYlKV6/O+miHbjqpIr9FTe5+0no4hofAMbfA=="],
-
- "babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="],
-
- "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="],
-
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
- "baseline-browser-mapping": ["baseline-browser-mapping@2.11.25", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw=="],
-
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"bin-links": ["bin-links@6.0.2", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w=="],
@@ -437,17 +343,15 @@
"brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="],
- "browserslist": ["browserslist@4.29.0", "", { "dependencies": { "baseline-browser-mapping": "^2.11.23", "caniuse-lite": "^1.0.30001810", "electron-to-chromium": "^1.5.427", "node-releases": "^2.0.55", "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA=="],
-
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
"bun-ffi-structs": ["bun-ffi-structs@0.3.1", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="],
"bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="],
- "cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="],
+ "bundle-name": ["bundle-name@4.1.1", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA=="],
- "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="],
+ "cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
@@ -465,8 +369,6 @@
"content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
- "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
-
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
@@ -477,6 +379,12 @@
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+ "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="],
+
+ "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
+
+ "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
+
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
@@ -487,18 +395,10 @@
"effect": ["effect@4.0.0-rc.112", "", { "dependencies": { "fast-check": "^4.9.0", "msgpackr": "^2.0.5" } }, "sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A=="],
- "electron-to-chromium": ["electron-to-chromium@1.5.434", "", {}, "sha512-7EeFW9OLf1NN9NKQP9xdOE/XKqjxv7JDPP6AEAkHli11+Fae1OaLoNuAf13hQv1PPNtuES0pLQSPk2+fS3s8ww=="],
-
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
- "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
-
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
- "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
-
- "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
-
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
@@ -509,26 +409,16 @@
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
- "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
-
- "find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
-
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
- "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
-
- "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
-
"gaxios": ["gaxios@7.3.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ=="],
"gcp-metadata": ["gcp-metadata@8.1.4", "", { "dependencies": { "gaxios": "7.1.3", "google-logging-utils": "1.1.3", "json-bigint": "^1.0.0" } }, "sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw=="],
- "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
-
"get-east-asian-width": ["get-east-asian-width@1.7.0", "", {}, "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q=="],
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
@@ -541,12 +431,8 @@
"gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="],
- "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
-
"hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
- "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
-
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
@@ -561,17 +447,19 @@
"ip-address": ["ip-address@10.7.2", "", {}, "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w=="],
- "is-core-module": ["is-core-module@2.17.0", "", { "dependencies": { "hasown": "^2.0.4" } }, "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA=="],
+ "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
- "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+ "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="],
- "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
+ "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
+
+ "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
- "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
- "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+ "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
@@ -581,8 +469,6 @@
"json-stringify-nice": ["json-stringify-nice@1.1.4", "", {}, "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw=="],
- "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
-
"jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="],
"just-diff": ["just-diff@6.0.2", "", {}, "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA=="],
@@ -593,10 +479,6 @@
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
- "locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
-
- "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
-
"lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="],
"make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="],
@@ -641,8 +523,6 @@
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
- "node-releases": ["node-releases@2.0.56", "", {}, "sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A=="],
-
"nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="],
"npm-bundled": ["npm-bundled@5.0.0", "", { "dependencies": { "npm-normalize-package-bin": "^5.0.0" } }, "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw=="],
@@ -659,42 +539,30 @@
"npm-registry-fetch": ["npm-registry-fetch@19.1.1", "", { "dependencies": { "@npmcli/redact": "^4.0.0", "jsonparse": "^1.3.1", "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minizlib": "^3.0.1", "npm-package-arg": "^13.0.0", "proc-log": "^6.0.0" } }, "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw=="],
+ "open": ["open@11.0.4", "", { "dependencies": { "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.2.1", "wsl-utils": "^1.0.0" } }, "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg=="],
+
"oxfmt": ["oxfmt@0.70.0", "", { "dependencies": { "tinypool": "2.1.2" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.70.0", "@oxfmt/binding-android-arm64": "0.70.0", "@oxfmt/binding-darwin-arm64": "0.70.0", "@oxfmt/binding-darwin-x64": "0.70.0", "@oxfmt/binding-freebsd-x64": "0.70.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.70.0", "@oxfmt/binding-linux-arm-musleabihf": "0.70.0", "@oxfmt/binding-linux-arm64-gnu": "0.70.0", "@oxfmt/binding-linux-arm64-musl": "0.70.0", "@oxfmt/binding-linux-ppc64-gnu": "0.70.0", "@oxfmt/binding-linux-riscv64-gnu": "0.70.0", "@oxfmt/binding-linux-riscv64-musl": "0.70.0", "@oxfmt/binding-linux-s390x-gnu": "0.70.0", "@oxfmt/binding-linux-x64-gnu": "0.70.0", "@oxfmt/binding-linux-x64-musl": "0.70.0", "@oxfmt/binding-openharmony-arm64": "0.70.0", "@oxfmt/binding-win32-arm64-msvc": "0.70.0", "@oxfmt/binding-win32-ia32-msvc": "0.70.0", "@oxfmt/binding-win32-x64-msvc": "0.70.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-IsHxZ4y0wQLLMhnrJblBJgZsLDzfULrJnAw5j/QqsTlMa/m3AqsbToi+W71uhBGaqlqq/PbbjvHc09TJwdv3Tw=="],
"oxlint": ["oxlint@1.85.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.85.0", "@oxlint/binding-android-arm64": "1.85.0", "@oxlint/binding-darwin-arm64": "1.85.0", "@oxlint/binding-darwin-x64": "1.85.0", "@oxlint/binding-freebsd-x64": "1.85.0", "@oxlint/binding-linux-arm-gnueabihf": "1.85.0", "@oxlint/binding-linux-arm-musleabihf": "1.85.0", "@oxlint/binding-linux-arm64-gnu": "1.85.0", "@oxlint/binding-linux-arm64-musl": "1.85.0", "@oxlint/binding-linux-ppc64-gnu": "1.85.0", "@oxlint/binding-linux-riscv64-gnu": "1.85.0", "@oxlint/binding-linux-riscv64-musl": "1.85.0", "@oxlint/binding-linux-s390x-gnu": "1.85.0", "@oxlint/binding-linux-x64-gnu": "1.85.0", "@oxlint/binding-linux-x64-musl": "1.85.0", "@oxlint/binding-openharmony-arm64": "1.85.0", "@oxlint/binding-win32-arm64-msvc": "1.85.0", "@oxlint/binding-win32-ia32-msvc": "1.85.0", "@oxlint/binding-win32-x64-msvc": "1.85.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-bc26s97nuvPj1ViyPsqmKecVkUWFMEdtayO8MaQ6oiLfs1pj94cQlZZhrh4BPNlr9HQosjhIlwgZKsfcwmcNgg=="],
- "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
-
- "p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
-
"p-map": ["p-map@7.0.8", "", {}, "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw=="],
- "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
-
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
"pacote": ["pacote@21.5.1", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "@npmcli/run-script": "^10.0.0", "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", "npm-package-arg": "^13.0.0", "npm-packlist": "^10.0.1", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^6.0.0", "sigstore": "^4.0.0", "ssri": "^13.0.0", "tar": "^7.4.3" }, "bin": { "pacote": "bin/index.js" } }, "sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg=="],
"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=="],
- "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
-
- "path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
-
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
- "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
-
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
- "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
-
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
- "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="],
-
"postcss-selector-parser": ["postcss-selector-parser@7.1.6", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw=="],
+ "powershell-utils": ["powershell-utils@0.2.1", "", {}, "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A=="],
+
"proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
"proggy": ["proggy@4.0.0", "", {}, "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ=="],
@@ -703,21 +571,15 @@
"promise-call-limit": ["promise-call-limit@3.0.2", "", {}, "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw=="],
- "protobufjs": ["protobufjs@7.6.6", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg=="],
-
"pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="],
"read-cmd-shim": ["read-cmd-shim@6.0.0", "", {}, "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A=="],
"redis": ["redis@6.2.1", "", { "dependencies": { "@redis/bloom": "6.2.1", "@redis/client": "6.2.1", "@redis/json": "6.2.1", "@redis/search": "6.2.1", "@redis/time-series": "6.2.1" } }, "sha512-Z9VHtgYs48PiQC77X9O2Er8Hj4T+5BtFjT91/vi5Is1D04N72cA946ZslM1ImJw8ZctFBZWAVjM7S5wJNeHMpg=="],
- "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="],
-
- "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
-
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
- "s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="],
+ "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
@@ -761,8 +623,6 @@
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
- "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
-
"tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
@@ -781,8 +641,6 @@
"undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
- "update-browserslist-db": ["update-browserslist-db@1.3.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ=="],
-
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
@@ -803,6 +661,8 @@
"ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
+ "wsl-utils": ["wsl-utils@1.0.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.2.0" } }, "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ=="],
+
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
@@ -811,14 +671,6 @@
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
- "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
-
- "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
-
- "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
-
- "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
-
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@npmcli/git/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
@@ -827,10 +679,6 @@
"@opencode/theme/@opentui/core": ["@opentui/core@0.5.10", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.10", "@opentui/core-darwin-x64": "0.5.10", "@opentui/core-linux-arm64": "0.5.10", "@opentui/core-linux-arm64-musl": "0.5.10", "@opentui/core-linux-x64": "0.5.10", "@opentui/core-linux-x64-musl": "0.5.10", "@opentui/core-win32-arm64": "0.5.10", "@opentui/core-win32-x64": "0.5.10" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-C3a2UbmefeAjIxAgm4BqjuSxKT4oqutfvYFwVvUgMxmGRHkNbBc/s7sukV0JgwcxFcV3uMFrXxo+E+BQtvuOiw=="],
- "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
-
- "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
-
"gcp-metadata/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="],
"gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
@@ -843,8 +691,6 @@
"node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
- "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
-
"rimraf/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=="],
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -865,8 +711,6 @@
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
- "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
-
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@npmcli/git/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
@@ -889,12 +733,6 @@
"@opencode/theme/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.10", "", { "os": "win32", "cpu": "x64" }, "sha512-u3KHa7kEeWrmKVDRJYpxSGO+g5E9cMGlrmTsPN3GVPHUmQMiREUawLXUvsU8+IHaQnqG3Q5nuE1yf4fPBzS+Qw=="],
- "babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="],
-
- "babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
-
- "babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
-
"minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
@@ -913,18 +751,10 @@
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
- "babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="],
-
- "babel-plugin-module-resolver/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
-
- "babel-plugin-module-resolver/glob/path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
-
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="],
"rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
- "babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
-
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
}
}
diff --git a/native/src/duck.rs b/native/src/duck.rs
index d21e04d..439cab3 100644
--- a/native/src/duck.rs
+++ b/native/src/duck.rs
@@ -382,6 +382,69 @@ mod imp {
set: u64,
}
+ // PipeWire's ALSA bridge can omit application.process.id. In that case the
+ // helper's stable stream names are safer than treating it as another app.
+ fn is_call_stream(stream: &serde_json::Value, own: &str) -> bool {
+ let pid = stream
+ .pointer("/properties/application.process.id")
+ .and_then(|pid| pid.as_str());
+ if let Some(pid) = pid {
+ return pid == own;
+ }
+ [
+ ("application.name", "PipeWire ALSA [gpt-live-host]"),
+ ("node.name", "alsa_playback.gpt-live-host"),
+ ]
+ .iter()
+ .any(|(key, expected)| {
+ stream
+ .get("properties")
+ .and_then(|p| p.get(key))
+ .and_then(|v| v.as_str())
+ == Some(*expected)
+ })
+ }
+
+ #[cfg(test)]
+ mod tests {
+ use super::is_call_stream;
+ use serde_json::json;
+
+ #[test]
+ fn excludes_own_pid() {
+ assert!(is_call_stream(
+ &json!({"properties": {"application.process.id": "42"}}),
+ "42"
+ ));
+ }
+
+ #[test]
+ fn excludes_pipewire_alsa_without_pid() {
+ for properties in [
+ json!({"application.name": "PipeWire ALSA [gpt-live-host]"}),
+ json!({"node.name": "alsa_playback.gpt-live-host"}),
+ ] {
+ assert!(is_call_stream(&json!({"properties": properties}), "42"));
+ }
+ }
+
+ #[test]
+ fn keeps_other_apps_and_prefers_known_pid() {
+ assert!(!is_call_stream(
+ &json!({"properties": {"application.name": "Music"}}),
+ "42"
+ ));
+ assert!(!is_call_stream(
+ &json!({"properties": {
+ "application.process.id": "99",
+ "application.name": "PipeWire ALSA [gpt-live-host]"
+ }}),
+ "42"
+ ));
+ assert!(!is_call_stream(&json!({"properties": {}}), "42"));
+ }
+ }
+
/// Other processes' playback streams as (index, volume in PulseAudio units).
fn streams() -> anyhow::Result> {
let output = Command::new("pactl")
@@ -395,10 +458,7 @@ mod imp {
.into_iter()
.flatten()
.filter_map(|stream| {
- let pid = stream
- .pointer("/properties/application.process.id")
- .and_then(|pid| pid.as_str());
- if pid == Some(own.as_str()) {
+ if is_call_stream(stream, &own) {
return None;
}
let index = stream.get("index")?.as_u64()?;
diff --git a/package.json b/package.json
index c92f7e1..8605298 100644
--- a/package.json
+++ b/package.json
@@ -52,11 +52,11 @@
"e2e": "bun scripts/e2e-call.ts"
},
"dependencies": {
- "@opencode/plugin": "^2.0.14",
+ "@opencode/plugin": "^2.0.20",
"zod": "4.1.8"
},
"devDependencies": {
- "@opencode/client": "2.0.14",
+ "@opencode/client": "2.0.20",
"@opencode/theme": "2.0.14",
"@opentui/core": "0.5.12",
"@types/bun": "latest",
@@ -68,6 +68,6 @@
"@opentui/core": ">=0.5.10"
},
"engines": {
- "opencode": ">=2.0.14"
+ "opencode": ">=2.0.20"
}
}
diff --git a/src/tui/controller.ts b/src/tui/controller.ts
index 739cf0f..c1f6158 100644
--- a/src/tui/controller.ts
+++ b/src/tui/controller.ts
@@ -2,6 +2,7 @@ import type { Plugin } from "@opencode/plugin/tui"
import { GptLive, type TaskStatus, type Voice } from "../shared/rpc"
import { HelperProcess, ensureHelper, type HelperEvent } from "./helper"
+import { syncCallModel } from "./model"
import { arrivalsFor, type Phase } from "./visuals"
type Context = Plugin.Context
@@ -166,6 +167,7 @@ export class VoiceController {
})
this.notice("Connecting to GPT-Live…", "info")
try {
+ await syncCallModel(this.context, sessionID)
const binary = await ensureHelper((message) => this.context.ui.toast.show({ message, variant: "info" }))
const helper = new HelperProcess(binary, {
onEvent: (event) => this.onHelperEvent(event),
diff --git a/src/tui/model.ts b/src/tui/model.ts
new file mode 100644
index 0000000..9c1bef7
--- /dev/null
+++ b/src/tui/model.ts
@@ -0,0 +1,17 @@
+import type { Plugin } from "@opencode/plugin/tui"
+
+type Context = Plugin.Context
+
+/** Persist the composer selection before voice can enqueue a prompt without a model. */
+export async function syncCallModel(context: Pick, sessionID: string) {
+ const selected = context.ui.model.current()
+ if (!selected) throw new Error("Choose a model before starting a voice call.")
+ await context.client.session.switchModel({
+ sessionID,
+ model: {
+ providerID: selected.providerID,
+ id: selected.modelID,
+ ...(selected.variant ? { variant: selected.variant } : {}),
+ },
+ })
+}
diff --git a/test/model.test.ts b/test/model.test.ts
new file mode 100644
index 0000000..4c6d7ce
--- /dev/null
+++ b/test/model.test.ts
@@ -0,0 +1,45 @@
+import { describe, expect, test } from "bun:test"
+
+import { syncCallModel } from "../src/tui/model"
+
+type Context = Parameters[0]
+
+function fixture(selected?: { providerID: string; modelID: string; variant?: string }, fail = false) {
+ const calls: unknown[] = []
+ const context = {
+ ui: { model: { current: () => selected } },
+ client: {
+ session: {
+ switchModel: async (input: unknown) => {
+ calls.push(input)
+ if (fail) throw new Error("session unavailable")
+ },
+ },
+ },
+ } as unknown as Context
+ return { context, calls }
+}
+
+describe("voice coding-session model", () => {
+ test("persists a new session's composer selection without sending a written prompt", async () => {
+ const { context, calls } = fixture({ providerID: "opencode", modelID: "free-model" })
+ await syncCallModel(context, "new-session")
+ expect(calls).toEqual([{ sessionID: "new-session", model: { providerID: "opencode", id: "free-model" } }])
+ })
+ test("preserves the selected variant and targets only the coding session", async () => {
+ const { context, calls } = fixture({ providerID: "openai", modelID: "reasoner", variant: "high" })
+ await syncCallModel(context, "coding-session")
+ expect(calls).toEqual([
+ { sessionID: "coding-session", model: { providerID: "openai", id: "reasoner", variant: "high" } },
+ ])
+ })
+ test("does not fall back to another provider when no model is selected", async () => {
+ const { context, calls } = fixture()
+ await expect(syncCallModel(context, "new-session")).rejects.toThrow("Choose a model")
+ expect(calls).toEqual([])
+ })
+ test("awaits persistence and propagates failures before a call can start", async () => {
+ const { context } = fixture({ providerID: "opencode", modelID: "free-model" }, true)
+ await expect(syncCallModel(context, "new-session")).rejects.toThrow("session unavailable")
+ })
+})
From 543123a684b2e9fdc112575f95ed8f2d91f882bd Mon Sep 17 00:00:00 2001
From: Franco Carballar
Date: Wed, 30 Sep 2026 17:11:22 -0300
Subject: [PATCH 2/6] test: place Linux duck regressions after implementation
---
native/src/duck.rs | 79 +++++++++++++++++++++++-----------------------
1 file changed, 39 insertions(+), 40 deletions(-)
diff --git a/native/src/duck.rs b/native/src/duck.rs
index 439cab3..36f430a 100644
--- a/native/src/duck.rs
+++ b/native/src/duck.rs
@@ -405,46 +405,6 @@ mod imp {
})
}
- #[cfg(test)]
- mod tests {
- use super::is_call_stream;
- use serde_json::json;
-
- #[test]
- fn excludes_own_pid() {
- assert!(is_call_stream(
- &json!({"properties": {"application.process.id": "42"}}),
- "42"
- ));
- }
-
- #[test]
- fn excludes_pipewire_alsa_without_pid() {
- for properties in [
- json!({"application.name": "PipeWire ALSA [gpt-live-host]"}),
- json!({"node.name": "alsa_playback.gpt-live-host"}),
- ] {
- assert!(is_call_stream(&json!({"properties": properties}), "42"));
- }
- }
-
- #[test]
- fn keeps_other_apps_and_prefers_known_pid() {
- assert!(!is_call_stream(
- &json!({"properties": {"application.name": "Music"}}),
- "42"
- ));
- assert!(!is_call_stream(
- &json!({"properties": {
- "application.process.id": "99",
- "application.name": "PipeWire ALSA [gpt-live-host]"
- }}),
- "42"
- ));
- assert!(!is_call_stream(&json!({"properties": {}}), "42"));
- }
- }
-
/// Other processes' playback streams as (index, volume in PulseAudio units).
fn streams() -> anyhow::Result> {
let output = Command::new("pactl")
@@ -513,6 +473,45 @@ mod imp {
}
Ok(())
}
+ #[cfg(test)]
+ mod tests {
+ use super::is_call_stream;
+ use serde_json::json;
+
+ #[test]
+ fn excludes_own_pid() {
+ assert!(is_call_stream(
+ &json!({"properties": {"application.process.id": "42"}}),
+ "42"
+ ));
+ }
+
+ #[test]
+ fn excludes_pipewire_alsa_without_pid() {
+ for properties in [
+ json!({"application.name": "PipeWire ALSA [gpt-live-host]"}),
+ json!({"node.name": "alsa_playback.gpt-live-host"}),
+ ] {
+ assert!(is_call_stream(&json!({"properties": properties}), "42"));
+ }
+ }
+
+ #[test]
+ fn keeps_other_apps_and_prefers_known_pid() {
+ assert!(!is_call_stream(
+ &json!({"properties": {"application.name": "Music"}}),
+ "42"
+ ));
+ assert!(!is_call_stream(
+ &json!({"properties": {
+ "application.process.id": "99",
+ "application.name": "PipeWire ALSA [gpt-live-host]"
+ }}),
+ "42"
+ ));
+ assert!(!is_call_stream(&json!({"properties": {}}), "42"));
+ }
+ }
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
From 6b6b1e7c7d5f348ed6b818d085e5abcd96a14377 Mon Sep 17 00:00:00 2001
From: Franco Carballar
Date: Wed, 30 Sep 2026 17:12:08 -0300
Subject: [PATCH 3/6] fix: retain composer model when voice creates a session
---
src/tui/index.ts | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/tui/index.ts b/src/tui/index.ts
index 478c0ae..f38c2c7 100644
--- a/src/tui/index.ts
+++ b/src/tui/index.ts
@@ -76,7 +76,16 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
const openNewSession = async () => {
try {
const location = context.location ?? context.data.location.default()
- const created = await context.client.session.create({ location })
+ const selected = context.ui.model.current()
+ if (!selected) throw new Error("Choose a model before starting a voice call.")
+ const created = await context.client.session.create({
+ location,
+ model: {
+ providerID: selected.providerID,
+ id: selected.modelID,
+ ...(selected.variant ? { variant: selected.variant } : {}),
+ },
+ })
const previous = context.renderer.currentFocusedEditor
context.ui.router.navigate({ type: "session", sessionID: created.id })
// Wait for the new session's prompt to mount and take focus, so opening the panel
From d183f1f7424484e7d84b1f60c3a33164c286be10 Mon Sep 17 00:00:00 2001
From: Franco Carballar
Date: Wed, 30 Sep 2026 18:58:09 -0300
Subject: [PATCH 4/6] fix: preserve the captured voice model through Home
startup
Use the same model and variant for session creation and persistence. Do not start on a session the user left during preparation.
Co-authored-by: OpenAI
---
src/tui/controller.ts | 6 +-
src/tui/index.ts | 26 +++----
src/tui/model.ts | 20 +++--
test/controller.test.ts | 167 ++++++++++++++++++++++++++++++++++++++++
test/model.test.ts | 24 ++++--
5 files changed, 214 insertions(+), 29 deletions(-)
create mode 100644 test/controller.test.ts
diff --git a/src/tui/controller.ts b/src/tui/controller.ts
index c1f6158..0c9df5b 100644
--- a/src/tui/controller.ts
+++ b/src/tui/controller.ts
@@ -2,7 +2,7 @@ import type { Plugin } from "@opencode/plugin/tui"
import { GptLive, type TaskStatus, type Voice } from "../shared/rpc"
import { HelperProcess, ensureHelper, type HelperEvent } from "./helper"
-import { syncCallModel } from "./model"
+import { syncCallModel, type CallModel } from "./model"
import { arrivalsFor, type Phase } from "./visuals"
type Context = Plugin.Context
@@ -153,7 +153,7 @@ export class VoiceController {
)
}
- async start(sessionID: string, voice?: Voice, fresh = false) {
+ async start(sessionID: string, model: CallModel, voice?: Voice, fresh = false) {
if (this.active || this.starting) return
this.starting = true
const session = this.context.data.session.get(sessionID) as { location?: Location } | undefined
@@ -167,7 +167,7 @@ export class VoiceController {
})
this.notice("Connecting to GPT-Live…", "info")
try {
- await syncCallModel(this.context, sessionID)
+ await syncCallModel(this.context, sessionID, model)
const binary = await ensureHelper((message) => this.context.ui.toast.show({ message, variant: "info" }))
const helper = new HelperProcess(binary, {
onEvent: (event) => this.onHelperEvent(event),
diff --git a/src/tui/index.ts b/src/tui/index.ts
index f38c2c7..b278878 100644
--- a/src/tui/index.ts
+++ b/src/tui/index.ts
@@ -1,7 +1,8 @@
import type { Plugin as PluginNamespace } from "@opencode/plugin/tui"
import { VOICES, type Voice } from "../shared/rpc"
-import { VoiceController } from "./controller"
+import { VoiceController, describeError } from "./controller"
+import { callModel, type CallModel } from "./model"
import { Frames, footerBadge, transcriptPanel, useCore, voiceAura, voiceStrip, type Core, type View } from "./ui"
const PANEL = "gptlive.transcript"
@@ -73,18 +74,12 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
}
// With no session open (e.g. on the home screen), a call starts in a new session.
- const openNewSession = async () => {
+ const openNewSession = async (model: CallModel) => {
try {
const location = context.location ?? context.data.location.default()
- const selected = context.ui.model.current()
- if (!selected) throw new Error("Choose a model before starting a voice call.")
const created = await context.client.session.create({
location,
- model: {
- providerID: selected.providerID,
- id: selected.modelID,
- ...(selected.variant ? { variant: selected.variant } : {}),
- },
+ model,
})
const previous = context.renderer.currentFocusedEditor
context.ui.router.navigate({ type: "session", sessionID: created.id })
@@ -107,10 +102,15 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
}
const start = async (chosen?: Voice, fresh = false) => {
- const sessionID = currentSession() ?? (await openNewSession())
- if (!sessionID) return
- if (autoPanel) openPanelKeepingFocus()
- await voice.start(sessionID, chosen, fresh)
+ try {
+ const model = callModel(context)
+ const sessionID = currentSession() ?? (await openNewSession(model))
+ if (!sessionID || currentSession() !== sessionID) return
+ if (autoPanel) openPanelKeepingFocus()
+ await voice.start(sessionID, model, chosen, fresh)
+ } catch (error) {
+ context.ui.toast.show({ title: "GPT-Live", message: describeError(error), variant: "error" })
+ }
}
// Close the transcript panel when a call ends normally; keep it open after a failure
diff --git a/src/tui/model.ts b/src/tui/model.ts
index 9c1bef7..8aaafa0 100644
--- a/src/tui/model.ts
+++ b/src/tui/model.ts
@@ -1,17 +1,23 @@
import type { Plugin } from "@opencode/plugin/tui"
type Context = Plugin.Context
+export type CallModel = Parameters[0]["model"]
-/** Persist the composer selection before voice can enqueue a prompt without a model. */
-export async function syncCallModel(context: Pick, sessionID: string) {
+/** Copy the selection before navigating or waiting for a session to be created. */
+export function callModel(context: Pick): CallModel {
const selected = context.ui.model.current()
if (!selected) throw new Error("Choose a model before starting a voice call.")
+ return {
+ providerID: selected.providerID,
+ id: selected.modelID,
+ ...(selected.variant ? { variant: selected.variant } : {}),
+ }
+}
+
+/** Persist the captured selection before voice can enqueue a prompt without a model. */
+export async function syncCallModel(context: Pick, sessionID: string, model: CallModel) {
await context.client.session.switchModel({
sessionID,
- model: {
- providerID: selected.providerID,
- id: selected.modelID,
- ...(selected.variant ? { variant: selected.variant } : {}),
- },
+ model,
})
}
diff --git a/test/controller.test.ts b/test/controller.test.ts
new file mode 100644
index 0000000..a02f5ef
--- /dev/null
+++ b/test/controller.test.ts
@@ -0,0 +1,167 @@
+import { afterEach, describe, expect, jest, mock, spyOn, test } from "bun:test"
+
+import { VoiceController } from "../src/tui/controller"
+import * as helpers from "../src/tui/helper"
+import { createTuiPlugin } from "../src/tui/index"
+import type { Core } from "../src/tui/ui"
+
+type Context = ConstructorParameters[0]
+type Selection = NonNullable>
+
+const cleanups: Array<() => void | Promise> = []
+
+afterEach(async () => {
+ await Promise.all(cleanups.splice(0).map((cleanup) => cleanup()))
+ mock.restore()
+ jest.useRealTimers()
+})
+
+function deferred() {
+ let resolve!: (value: T) => void
+ let reject!: (error: Error) => void
+ const promise = new Promise((yes, no) => {
+ resolve = yes
+ reject = no
+ })
+ return { promise, resolve, reject }
+}
+
+function fixture(home = false) {
+ jest.useFakeTimers()
+ const selected = { providerID: "openai", modelID: "reasoner", variant: "high" }
+ const model = { providerID: "openai", id: "reasoner", variant: "high" }
+ const selection = jest.fn((): Selection | undefined => selected)
+ const location = { directory: "/project" }
+ const call = { callID: "call", voiceSessionID: "voice", previous: [], sdp: "answer", voice: "cove", model: "live" }
+ const rpc = {
+ events: { on: () => () => {} },
+ start: jest.fn(async () => call),
+ stop: jest.fn(async () => ({ stopped: true })),
+ alive: jest.fn(async () => ({ active: true })),
+ }
+ const session = {
+ create: jest.fn(async () => ({ id: "created" })),
+ switchModel: jest.fn(async () => {}),
+ }
+ const ensure = spyOn(helpers, "ensureHelper").mockResolvedValue("fake")
+ const helper = {
+ start: jest.fn(async () => "offer"),
+ answer: jest.fn(async () => {}),
+ close: jest.fn(async () => {}),
+ kill: jest.fn(() => {}),
+ }
+ const callbacks: helpers.HelperCallbacks[] = []
+ const constructors = helpers as unknown as {
+ HelperProcess: (binary: string, hooks?: helpers.HelperCallbacks) => helpers.HelperProcess
+ }
+ const spawn = spyOn(constructors, "HelperProcess").mockImplementation(function (_binary, hooks) {
+ callbacks.push(hooks ?? {})
+ return helper as unknown as helpers.HelperProcess
+ })
+ let route: { type: "home" } | { type: "session"; sessionID: string } = home
+ ? { type: "home" }
+ : { type: "session", sessionID: "main" }
+ const renderer = { currentFocusedEditor: { isDestroyed: false } }
+ const commands: Array<{ id: string; run: () => Promise }> = []
+ const navigate = jest.fn((next: typeof route) => {
+ route = next
+ renderer.currentFocusedEditor = { isDestroyed: false }
+ })
+ const panel = { current: () => undefined, close: jest.fn(), open: jest.fn(() => true) }
+ const toast = jest.fn()
+ const context = {
+ options: { panel: false },
+ data: { session: { get: () => ({ location }) }, location: { default: () => location } },
+ renderer,
+ keymap: {
+ shortcuts: () => [],
+ layer: (read: () => { commands: typeof commands }) => commands.push(...read().commands),
+ },
+ ui: {
+ model: { current: selection },
+ router: { current: () => route, navigate },
+ panel,
+ toast: { show: toast },
+ slot: (slot: { append?: string; render: () => void }) => {
+ if (slot.append === "app") slot.render()
+ return () => {}
+ },
+ },
+ client: { session, rpc: () => rpc },
+ } as unknown as Context
+ const controller = new VoiceController(context, {})
+ cleanups.push(() => controller.dispose())
+ return {
+ context,
+ controller,
+ model,
+ selected,
+ selection,
+ location,
+ rpc,
+ session,
+ ensure,
+ helper,
+ callbacks,
+ spawn,
+ navigate,
+ panel,
+ toast,
+ commands,
+ route: (next: typeof route) => {
+ route = next
+ },
+ }
+}
+
+async function plugin(f: ReturnType) {
+ const cleanup = await createTuiPlugin({} as Core).setup(f.context)
+ if (typeof cleanup === "function") cleanups.push(cleanup)
+ return (id: string) => {
+ const command = f.commands.find((item) => item.id === id)
+ if (!command) throw new Error(`Missing command: ${id}`)
+ return command.run()
+ }
+}
+
+describe("voice model during Home startup", () => {
+ test("uses one copied selection for creation and persistence across navigation", async () => {
+ const f = fixture(true)
+ const command = await plugin(f)
+ const created = deferred<{ id: string }>()
+ f.session.create.mockImplementation(() => created.promise)
+ const starting = command("gptlive.toggle")
+ f.selected.providerID = "other"
+ f.selected.modelID = "other-model"
+ f.selected.variant = "low"
+ created.resolve({ id: "created" })
+ await created.promise
+ jest.advanceTimersByTime(25)
+ await starting
+ expect(f.selection).toHaveBeenCalledTimes(1)
+ expect(f.session.create).toHaveBeenCalledWith({ location: f.location, model: f.model })
+ expect(f.session.switchModel).toHaveBeenCalledWith({ sessionID: "created", model: f.model })
+ expect(f.rpc.start).toHaveBeenCalledWith(
+ { sessionID: "created", sdp: "offer", voice: undefined, fresh: false },
+ { location: f.location },
+ )
+ })
+
+ test("does not start or persist another session's model after leaving the created session", async () => {
+ const f = fixture(true)
+ const command = await plugin(f)
+ f.navigate.mockImplementation(() => {
+ f.route({ type: "session", sessionID: "other" })
+ f.selection.mockReturnValue({ providerID: "other", modelID: "other-model" })
+ })
+ const starting = command("gptlive.toggle")
+ await Promise.resolve()
+ jest.advanceTimersByTime(2_000)
+ await starting
+ expect(f.selection).toHaveBeenCalledTimes(1)
+ expect(f.session.switchModel).not.toHaveBeenCalled()
+ expect(f.ensure).not.toHaveBeenCalled()
+ expect(f.rpc.start).not.toHaveBeenCalled()
+ expect(f.panel.open).not.toHaveBeenCalled()
+ })
+})
diff --git a/test/model.test.ts b/test/model.test.ts
index 4c6d7ce..2c81a68 100644
--- a/test/model.test.ts
+++ b/test/model.test.ts
@@ -1,8 +1,8 @@
import { describe, expect, test } from "bun:test"
-import { syncCallModel } from "../src/tui/model"
+import { callModel, syncCallModel } from "../src/tui/model"
-type Context = Parameters[0]
+type Context = Parameters[0] & Parameters[0]
function fixture(selected?: { providerID: string; modelID: string; variant?: string }, fail = false) {
const calls: unknown[] = []
@@ -23,23 +23,35 @@ function fixture(selected?: { providerID: string; modelID: string; variant?: str
describe("voice coding-session model", () => {
test("persists a new session's composer selection without sending a written prompt", async () => {
const { context, calls } = fixture({ providerID: "opencode", modelID: "free-model" })
- await syncCallModel(context, "new-session")
+ await syncCallModel(context, "new-session", callModel(context))
expect(calls).toEqual([{ sessionID: "new-session", model: { providerID: "opencode", id: "free-model" } }])
})
test("preserves the selected variant and targets only the coding session", async () => {
const { context, calls } = fixture({ providerID: "openai", modelID: "reasoner", variant: "high" })
- await syncCallModel(context, "coding-session")
+ await syncCallModel(context, "coding-session", callModel(context))
expect(calls).toEqual([
{ sessionID: "coding-session", model: { providerID: "openai", id: "reasoner", variant: "high" } },
])
})
test("does not fall back to another provider when no model is selected", async () => {
const { context, calls } = fixture()
- await expect(syncCallModel(context, "new-session")).rejects.toThrow("Choose a model")
+ expect(() => callModel(context)).toThrow("Choose a model")
expect(calls).toEqual([])
})
test("awaits persistence and propagates failures before a call can start", async () => {
const { context } = fixture({ providerID: "opencode", modelID: "free-model" }, true)
- await expect(syncCallModel(context, "new-session")).rejects.toThrow("session unavailable")
+ await expect(syncCallModel(context, "new-session", callModel(context))).rejects.toThrow("session unavailable")
+ })
+ test("copies the selection instead of retaining a live UI object", async () => {
+ const selected = { providerID: "openai", modelID: "reasoner", variant: "high" }
+ const { context, calls } = fixture(selected)
+ const model = callModel(context)
+ selected.providerID = "other"
+ selected.modelID = "other-model"
+ selected.variant = "low"
+ await syncCallModel(context, "coding-session", model)
+ expect(calls).toEqual([
+ { sessionID: "coding-session", model: { providerID: "openai", id: "reasoner", variant: "high" } },
+ ])
})
})
From e32d8585d8dd34b18e2a4ed523e894440d642075 Mon Sep 17 00:00:00 2001
From: Franco Carballar
Date: Wed, 30 Sep 2026 19:06:06 -0300
Subject: [PATCH 5/6] fix: invalidate cancelled voice startup attempts
Guard asynchronous startup and helper callbacks after stop or disposal. Stop late RPC replies by their returned call ID, and cancel Home preparation before navigation or model persistence.
Co-authored-by: OpenAI
---
src/tui/controller.ts | 35 ++++++--
src/tui/index.ts | 33 ++++++--
test/controller.test.ts | 178 ++++++++++++++++++++++++++++++++++++++--
3 files changed, 226 insertions(+), 20 deletions(-)
diff --git a/src/tui/controller.ts b/src/tui/controller.ts
index 0c9df5b..b9c0271 100644
--- a/src/tui/controller.ts
+++ b/src/tui/controller.ts
@@ -73,6 +73,8 @@ export class VoiceController {
private helper: HelperProcess | undefined
private readonly unsubscribe: Array<() => void> = []
private starting = false
+ private generation = 0
+ private disposed = false
constructor(
private readonly context: Context,
@@ -110,7 +112,8 @@ export class VoiceController {
private subscribe() {
const rpc = this.rpc()
- const mine = (event: { data: { callID: string } }) => event.data.callID === this.state.callID
+ const mine = (event: { data: { callID: string } }) =>
+ !this.disposed && this.active && this.state.phase !== "closing" && event.data.callID === this.state.callID
this.unsubscribe.push(
rpc.events.on("state", (event) => {
if (!mine(event)) return
@@ -154,8 +157,10 @@ export class VoiceController {
}
async start(sessionID: string, model: CallModel, voice?: Voice, fresh = false) {
- if (this.active || this.starting) return
+ if (this.disposed || this.active || this.starting) return
this.starting = true
+ const generation = ++this.generation
+ const current = () => !this.disposed && this.generation === generation
const session = this.context.data.session.get(sessionID) as { location?: Location } | undefined
const location = session?.location ?? this.context.location ?? this.context.data.location.default()
Object.assign(this.state, initial(), {
@@ -168,11 +173,17 @@ export class VoiceController {
this.notice("Connecting to GPT-Live…", "info")
try {
await syncCallModel(this.context, sessionID, model)
- const binary = await ensureHelper((message) => this.context.ui.toast.show({ message, variant: "info" }))
+ if (!current()) return
+ const binary = await ensureHelper((message) => {
+ if (current()) this.context.ui.toast.show({ message, variant: "info" })
+ })
+ if (!current()) return
const helper = new HelperProcess(binary, {
- onEvent: (event) => this.onHelperEvent(event),
+ onEvent: (event) => {
+ if (current() && this.helper === helper) this.onHelperEvent(event)
+ },
onExit: (code, stderr) => {
- if (this.helper !== helper) return
+ if (!current() || this.helper !== helper) return
this.helper = undefined
if (this.active)
void this.stop(code ? `Audio stopped unexpectedly: ${stderr.trim().split("\n").pop() ?? code}` : undefined)
@@ -188,10 +199,18 @@ export class VoiceController {
output: output ? (output === "none" ? "none" : { file: output }) : undefined,
duck: this.options.duck,
})
+ if (!current()) return
const call = await this.rpc().start(
{ sessionID, sdp: offer, voice: voice ?? this.options.voice, fresh },
{ location },
)
+ if (!current()) {
+ // stop() could not know the call ID while this request was pending.
+ await this.rpc()
+ .stop({ callID: call.callID }, { location })
+ .catch(() => undefined)
+ return
+ }
const past = call.previous.map((turn): Entry => ({
id: nextID(),
kind: turn.role,
@@ -213,15 +232,17 @@ export class VoiceController {
for (const notice of call.notices ?? []) this.notice(notice, "info")
this.heartbeat(call.callID)
await helper.answer(call.sdp)
+ if (!current()) return
if (this.state.phase === "connecting") this.markLive()
} catch (error) {
- await this.teardown(describeError(error))
+ if (current()) await this.teardown(describeError(error))
} finally {
this.starting = false
}
}
async stop(failure?: string) {
+ this.generation++
if (!this.active) return
this.set({ phase: "closing" })
const { callID, location } = this.state
@@ -236,6 +257,7 @@ export class VoiceController {
private async teardown(failure?: string, reason?: string) {
if (this.state.phase === "idle" || this.state.phase === "error" || this.tearingDown) return
+ this.generation++
this.tearingDown = true
try {
await this.finishTeardown(failure, reason)
@@ -384,6 +406,7 @@ export class VoiceController {
}
async dispose() {
+ this.disposed = true
for (const stop of this.unsubscribe.splice(0)) stop()
await this.stop()
this.listeners.clear()
diff --git a/src/tui/index.ts b/src/tui/index.ts
index b278878..65b3eef 100644
--- a/src/tui/index.ts
+++ b/src/tui/index.ts
@@ -67,6 +67,9 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
})
const frames = new Frames(voice)
const autoPanel = options.panel !== false
+ let preparing = false
+ let generation = 0
+ let disposed = false
const currentSession = () => {
const route = context.ui.router.current()
@@ -74,24 +77,27 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
}
// With no session open (e.g. on the home screen), a call starts in a new session.
- const openNewSession = async (model: CallModel) => {
+ const openNewSession = async (model: CallModel, attempt: number) => {
try {
const location = context.location ?? context.data.location.default()
const created = await context.client.session.create({
location,
model,
})
+ if (attempt !== generation) return undefined
const previous = context.renderer.currentFocusedEditor
context.ui.router.navigate({ type: "session", sessionID: created.id })
// Wait for the new session's prompt to mount and take focus, so opening the panel
// hands focus back to it rather than to the home screen's (now gone) prompt.
const ready = () => {
+ if (attempt !== generation) return true
const editor = context.renderer.currentFocusedEditor
return currentSession() === created.id && editor && editor !== previous && !editor.isDestroyed
}
await waitFor(ready, 2_000)
return created.id
} catch (error) {
+ if (attempt !== generation) return undefined
context.ui.toast.show({
title: "GPT-Live",
message: `Could not start a new session: ${error instanceof Error ? error.message : String(error)}`,
@@ -102,17 +108,28 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
}
const start = async (chosen?: Voice, fresh = false) => {
+ if (disposed || preparing || voice.active) return
+ preparing = true
+ const attempt = ++generation
try {
const model = callModel(context)
- const sessionID = currentSession() ?? (await openNewSession(model))
- if (!sessionID || currentSession() !== sessionID) return
+ const sessionID = currentSession() ?? (await openNewSession(model, attempt))
+ if (attempt !== generation || !sessionID || currentSession() !== sessionID) return
if (autoPanel) openPanelKeepingFocus()
await voice.start(sessionID, model, chosen, fresh)
} catch (error) {
- context.ui.toast.show({ title: "GPT-Live", message: describeError(error), variant: "error" })
+ if (attempt === generation)
+ context.ui.toast.show({ title: "GPT-Live", message: describeError(error), variant: "error" })
+ } finally {
+ preparing = false
}
}
+ const stop = () => {
+ generation++
+ return voice.stop()
+ }
+
// Close the transcript panel when a call ends normally; keep it open after a failure
// so the error stays readable.
let wasActive = false
@@ -136,10 +153,10 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
return opened
}
- const toggle = () => (voice.active ? voice.stop() : start())
+ const toggle = () => (voice.active || preparing ? stop() : start())
const restartWith = async (name: Voice, fresh = false) => {
- if (voice.active) await voice.stop()
+ if (voice.active || preparing) await stop()
await start(name, fresh)
}
@@ -187,7 +204,7 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
bind: key("stop"),
palette: true,
slash: { name: "voice-stop", aliases: ["hangup"] },
- run: () => voice.stop(),
+ run: () => stop(),
},
{
id: "gptlive.new",
@@ -284,6 +301,8 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
}
return async () => {
+ disposed = true
+ generation++
if (autostart) clearInterval(autostart)
stopWatchingCall()
for (const dispose of disposers) dispose()
diff --git a/test/controller.test.ts b/test/controller.test.ts
index a02f5ef..0f192c9 100644
--- a/test/controller.test.ts
+++ b/test/controller.test.ts
@@ -56,12 +56,12 @@ function fixture(home = false) {
}
const spawn = spyOn(constructors, "HelperProcess").mockImplementation(function (_binary, hooks) {
callbacks.push(hooks ?? {})
- return helper as unknown as helpers.HelperProcess
+ return { ...helper } as unknown as helpers.HelperProcess
})
let route: { type: "home" } | { type: "session"; sessionID: string } = home
? { type: "home" }
: { type: "session", sessionID: "main" }
- const renderer = { currentFocusedEditor: { isDestroyed: false } }
+ const renderer = { currentFocusedEditor: { isDestroyed: false }, focusRenderable: jest.fn() }
const commands: Array<{ id: string; run: () => Promise }> = []
const navigate = jest.fn((next: typeof route) => {
route = next
@@ -70,7 +70,7 @@ function fixture(home = false) {
const panel = { current: () => undefined, close: jest.fn(), open: jest.fn(() => true) }
const toast = jest.fn()
const context = {
- options: { panel: false },
+ options: { panel: home },
data: { session: { get: () => ({ location }) }, location: { default: () => location } },
renderer,
keymap: {
@@ -98,6 +98,7 @@ function fixture(home = false) {
selected,
selection,
location,
+ call,
rpc,
session,
ensure,
@@ -116,18 +117,20 @@ function fixture(home = false) {
async function plugin(f: ReturnType) {
const cleanup = await createTuiPlugin({} as Core).setup(f.context)
- if (typeof cleanup === "function") cleanups.push(cleanup)
- return (id: string) => {
+ if (typeof cleanup !== "function") throw new Error("Missing plugin cleanup")
+ cleanups.push(cleanup)
+ const run = (id: string) => {
const command = f.commands.find((item) => item.id === id)
if (!command) throw new Error(`Missing command: ${id}`)
return command.run()
}
+ return { run, dispose: cleanup }
}
describe("voice model during Home startup", () => {
test("uses one copied selection for creation and persistence across navigation", async () => {
const f = fixture(true)
- const command = await plugin(f)
+ const { run: command } = await plugin(f)
const created = deferred<{ id: string }>()
f.session.create.mockImplementation(() => created.promise)
const starting = command("gptlive.toggle")
@@ -141,6 +144,7 @@ describe("voice model during Home startup", () => {
expect(f.selection).toHaveBeenCalledTimes(1)
expect(f.session.create).toHaveBeenCalledWith({ location: f.location, model: f.model })
expect(f.session.switchModel).toHaveBeenCalledWith({ sessionID: "created", model: f.model })
+ expect(f.panel.open).toHaveBeenCalledTimes(1)
expect(f.rpc.start).toHaveBeenCalledWith(
{ sessionID: "created", sdp: "offer", voice: undefined, fresh: false },
{ location: f.location },
@@ -149,7 +153,7 @@ describe("voice model during Home startup", () => {
test("does not start or persist another session's model after leaving the created session", async () => {
const f = fixture(true)
- const command = await plugin(f)
+ const { run: command } = await plugin(f)
f.navigate.mockImplementation(() => {
f.route({ type: "session", sessionID: "other" })
f.selection.mockReturnValue({ providerID: "other", modelID: "other-model" })
@@ -164,4 +168,164 @@ describe("voice model during Home startup", () => {
expect(f.rpc.start).not.toHaveBeenCalled()
expect(f.panel.open).not.toHaveBeenCalled()
})
+
+ test("reports a missing selection without creating a session or helper", async () => {
+ const f = fixture(true)
+ const { run: command } = await plugin(f)
+ f.selection.mockReturnValue(undefined)
+ await command("gptlive.toggle")
+ expect(f.toast).toHaveBeenCalledWith({
+ title: "GPT-Live",
+ message: "Choose a model before starting a voice call.",
+ variant: "error",
+ })
+ expect(f.session.create).not.toHaveBeenCalled()
+ expect(f.ensure).not.toHaveBeenCalled()
+ })
+})
+
+describe("voice startup cancellation", () => {
+ test.each(["model", "binary", "offer", "call", "answer"] as const)(
+ "stop invalidates an attempt waiting for %s",
+ async (stage) => {
+ const f = fixture()
+ const reached = deferred()
+ const resume = deferred()
+ const wait = async () => {
+ reached.resolve()
+ await resume.promise
+ }
+ if (stage === "model") f.session.switchModel.mockImplementation(wait)
+ if (stage === "binary")
+ f.ensure.mockImplementation(async () => {
+ await wait()
+ return "fake"
+ })
+ if (stage === "offer")
+ f.helper.start.mockImplementation(async () => {
+ await wait()
+ return "offer"
+ })
+ if (stage === "call")
+ f.rpc.start.mockImplementation(async () => {
+ await wait()
+ return f.call
+ })
+ if (stage === "answer") f.helper.answer.mockImplementation(wait)
+ const starting = f.controller.start("main", f.model)
+ await reached.promise
+ await f.controller.stop()
+ const revision = f.controller.state.revision
+ const notices = f.toast.mock.calls.length
+ if (stage === "binary") f.ensure.mock.calls[0][0]?.("late download progress")
+ resume.resolve()
+ await starting
+ expect(f.controller.state.phase).toBe("idle")
+ expect(f.controller.state.revision).toBe(revision)
+ expect(f.toast).toHaveBeenCalledTimes(notices)
+ if (stage === "model" || stage === "binary") expect(f.spawn).not.toHaveBeenCalled()
+ if (stage === "model" || stage === "binary" || stage === "offer") expect(f.rpc.start).not.toHaveBeenCalled()
+ if (stage !== "answer") expect(f.helper.answer).not.toHaveBeenCalled()
+ if (stage === "offer" || stage === "call" || stage === "answer") expect(f.helper.close).toHaveBeenCalledTimes(1)
+ if (stage === "call" || stage === "answer")
+ expect(f.rpc.stop).toHaveBeenCalledWith({ callID: f.call.callID }, { location: f.location })
+ jest.advanceTimersByTime(5_000)
+ expect(f.rpc.alive).not.toHaveBeenCalled()
+ await f.controller.start("main", f.model)
+ expect(f.controller.state.phase).toBe("live")
+ },
+ )
+
+ test("dispose prevents a pending model sync and all subsequent starts from creating resources", async () => {
+ const f = fixture()
+ const pending = deferred()
+ f.session.switchModel.mockImplementation(() => pending.promise)
+ const starting = f.controller.start("main", f.model)
+ await f.controller.dispose()
+ pending.resolve()
+ await starting
+ await f.controller.start("main", f.model)
+ expect(f.session.switchModel).toHaveBeenCalledTimes(1)
+ expect(f.ensure).not.toHaveBeenCalled()
+ expect(f.spawn).not.toHaveBeenCalled()
+ expect(f.rpc.start).not.toHaveBeenCalled()
+ expect(f.controller.state.phase).toBe("idle")
+ })
+
+ test("ignores a cancelled attempt's late rejection", async () => {
+ const f = fixture()
+ const pending = deferred()
+ f.session.switchModel.mockImplementation(() => pending.promise)
+ const starting = f.controller.start("main", f.model)
+ await f.controller.stop()
+ const notices = f.toast.mock.calls.length
+ pending.reject(new Error("late failure"))
+ await starting
+ expect(f.controller.state.error).toBeUndefined()
+ expect(f.toast).toHaveBeenCalledTimes(notices)
+ expect(f.ensure).not.toHaveBeenCalled()
+ })
+
+ test("a persistence failure is visible and cannot start the helper", async () => {
+ const f = fixture()
+ f.session.switchModel.mockRejectedValue(new Error("session unavailable"))
+ await f.controller.start("main", f.model)
+ expect(f.controller.state.phase).toBe("error")
+ expect(f.controller.state.error).toBe("session unavailable")
+ expect(f.ensure).not.toHaveBeenCalled()
+ expect(f.rpc.start).not.toHaveBeenCalled()
+ expect(f.toast).toHaveBeenCalledWith(expect.objectContaining({ message: "session unavailable", variant: "error" }))
+ })
+
+ test("callbacks from an old helper cannot affect the next call", async () => {
+ const f = fixture()
+ await f.controller.start("main", f.model)
+ const old = f.callbacks[0]
+ await f.controller.stop()
+ await f.controller.start("main", f.model)
+ const revision = f.controller.state.revision
+ old.onEvent?.({ type: "warning", message: "late warning" })
+ old.onEvent?.({ type: "audio", input: { name: "old microphone" } })
+ old.onEvent?.({ type: "error", fatal: true, message: "late failure" })
+ old.onExit?.(1, "late exit")
+ expect(f.controller.state.phase).toBe("live")
+ expect(f.controller.state.revision).toBe(revision)
+ expect(f.helper.close).toHaveBeenCalledTimes(1)
+ })
+})
+
+describe("Home preparation cancellation", () => {
+ test.each(["stop", "dispose"] as const)("%s during creation prevents late navigation and startup", async (action) => {
+ const f = fixture(true)
+ const { run: command, dispose } = await plugin(f)
+ const created = deferred<{ id: string }>()
+ f.session.create.mockImplementation(() => created.promise)
+ const starting = command("gptlive.toggle")
+ if (action === "stop") await command("gptlive.stop")
+ else await dispose()
+ created.resolve({ id: "created" })
+ await starting
+ expect(f.navigate).not.toHaveBeenCalled()
+ expect(f.panel.open).not.toHaveBeenCalled()
+ expect(f.session.switchModel).not.toHaveBeenCalled()
+ expect(f.ensure).not.toHaveBeenCalled()
+ if (action === "dispose") {
+ await command("gptlive.toggle")
+ expect(f.session.create).toHaveBeenCalledTimes(1)
+ }
+ })
+
+ test("stop while waiting for focus prevents model persistence and helper startup", async () => {
+ const f = fixture(true)
+ const { run: command } = await plugin(f)
+ const starting = command("gptlive.toggle")
+ await Promise.resolve()
+ expect(f.navigate).toHaveBeenCalled()
+ await command("gptlive.stop")
+ jest.advanceTimersByTime(25)
+ await starting
+ expect(f.session.switchModel).not.toHaveBeenCalled()
+ expect(f.spawn).not.toHaveBeenCalled()
+ expect(f.rpc.start).not.toHaveBeenCalled()
+ })
})
From 826d50a3768c0f48c8788f4ea3efd2e36b951d6c Mon Sep 17 00:00:00 2001
From: Franco Carballar
Date: Tue, 6 Oct 2026 13:23:22 -0300
Subject: [PATCH 6/6] =?UTF-8?q?fix(tui):=20=F0=9F=90=9B=20permitir=20el=20?=
=?UTF-8?q?modelo=20predeterminado=20y=20reintentar=20voz?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Libera las trabas por intento al cancelar para que un retry inmediato o /voice-new durante la creación de sesión pueda arrancar.
Co-Authored-By: OpenAI
---
src/tui/controller.ts | 20 +++++++----
src/tui/index.ts | 19 +++++-----
src/tui/model.ts | 7 ++--
test/controller.test.ts | 77 ++++++++++++++++++++++++++++++++++++-----
test/model.test.ts | 5 +--
5 files changed, 99 insertions(+), 29 deletions(-)
diff --git a/src/tui/controller.ts b/src/tui/controller.ts
index b9c0271..56b2966 100644
--- a/src/tui/controller.ts
+++ b/src/tui/controller.ts
@@ -72,7 +72,7 @@ export class VoiceController {
private readonly listeners = new Set<() => void>()
private helper: HelperProcess | undefined
private readonly unsubscribe: Array<() => void> = []
- private starting = false
+ private starting: number | undefined
private generation = 0
private disposed = false
@@ -156,10 +156,10 @@ export class VoiceController {
)
}
- async start(sessionID: string, model: CallModel, voice?: Voice, fresh = false) {
- if (this.disposed || this.active || this.starting) return
- this.starting = true
+ async start(sessionID: string, model: CallModel | undefined, voice?: Voice, fresh = false) {
+ if (this.disposed || this.active || this.starting !== undefined) return
const generation = ++this.generation
+ this.starting = generation
const current = () => !this.disposed && this.generation === generation
const session = this.context.data.session.get(sessionID) as { location?: Location } | undefined
const location = session?.location ?? this.context.location ?? this.context.data.location.default()
@@ -237,12 +237,12 @@ export class VoiceController {
} catch (error) {
if (current()) await this.teardown(describeError(error))
} finally {
- this.starting = false
+ if (this.starting === generation) this.starting = undefined
}
}
async stop(failure?: string) {
- this.generation++
+ this.invalidateStart()
if (!this.active) return
this.set({ phase: "closing" })
const { callID, location } = this.state
@@ -255,9 +255,15 @@ export class VoiceController {
private tearingDown = false
+ private invalidateStart() {
+ const attempt = this.generation
+ this.generation++
+ if (this.starting === attempt) this.starting = undefined
+ }
+
private async teardown(failure?: string, reason?: string) {
if (this.state.phase === "idle" || this.state.phase === "error" || this.tearingDown) return
- this.generation++
+ this.invalidateStart()
this.tearingDown = true
try {
await this.finishTeardown(failure, reason)
diff --git a/src/tui/index.ts b/src/tui/index.ts
index 65b3eef..aaa576c 100644
--- a/src/tui/index.ts
+++ b/src/tui/index.ts
@@ -67,7 +67,7 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
})
const frames = new Frames(voice)
const autoPanel = options.panel !== false
- let preparing = false
+ let preparing: number | undefined
let generation = 0
let disposed = false
@@ -77,12 +77,12 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
}
// With no session open (e.g. on the home screen), a call starts in a new session.
- const openNewSession = async (model: CallModel, attempt: number) => {
+ const openNewSession = async (model: CallModel | undefined, attempt: number) => {
try {
const location = context.location ?? context.data.location.default()
const created = await context.client.session.create({
location,
- model,
+ ...(model ? { model } : {}),
})
if (attempt !== generation) return undefined
const previous = context.renderer.currentFocusedEditor
@@ -108,9 +108,9 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
}
const start = async (chosen?: Voice, fresh = false) => {
- if (disposed || preparing || voice.active) return
- preparing = true
+ if (disposed || preparing !== undefined || voice.active) return
const attempt = ++generation
+ preparing = attempt
try {
const model = callModel(context)
const sessionID = currentSession() ?? (await openNewSession(model, attempt))
@@ -121,12 +121,14 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
if (attempt === generation)
context.ui.toast.show({ title: "GPT-Live", message: describeError(error), variant: "error" })
} finally {
- preparing = false
+ if (preparing === attempt) preparing = undefined
}
}
const stop = () => {
+ const attempt = preparing
generation++
+ if (preparing === attempt) preparing = undefined
return voice.stop()
}
@@ -153,10 +155,10 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
return opened
}
- const toggle = () => (voice.active || preparing ? stop() : start())
+ const toggle = () => (voice.active || preparing !== undefined ? stop() : start())
const restartWith = async (name: Voice, fresh = false) => {
- if (voice.active || preparing) await stop()
+ if (voice.active || preparing !== undefined) await stop()
await start(name, fresh)
}
@@ -303,6 +305,7 @@ export function createTuiPlugin(module: Core): PluginNamespace.Definition {
return async () => {
disposed = true
generation++
+ preparing = undefined
if (autostart) clearInterval(autostart)
stopWatchingCall()
for (const dispose of disposers) dispose()
diff --git a/src/tui/model.ts b/src/tui/model.ts
index 8aaafa0..1e773f7 100644
--- a/src/tui/model.ts
+++ b/src/tui/model.ts
@@ -4,9 +4,9 @@ type Context = Plugin.Context
export type CallModel = Parameters[0]["model"]
/** Copy the selection before navigating or waiting for a session to be created. */
-export function callModel(context: Pick): CallModel {
+export function callModel(context: Pick): CallModel | undefined {
const selected = context.ui.model.current()
- if (!selected) throw new Error("Choose a model before starting a voice call.")
+ if (!selected) return undefined
return {
providerID: selected.providerID,
id: selected.modelID,
@@ -15,7 +15,8 @@ export function callModel(context: Pick): CallModel {
}
/** Persist the captured selection before voice can enqueue a prompt without a model. */
-export async function syncCallModel(context: Pick, sessionID: string, model: CallModel) {
+export async function syncCallModel(context: Pick, sessionID: string, model: CallModel | undefined) {
+ if (!model) return
await context.client.session.switchModel({
sessionID,
model,
diff --git a/test/controller.test.ts b/test/controller.test.ts
index 0f192c9..329c3a8 100644
--- a/test/controller.test.ts
+++ b/test/controller.test.ts
@@ -169,22 +169,50 @@ describe("voice model during Home startup", () => {
expect(f.panel.open).not.toHaveBeenCalled()
})
- test("reports a missing selection without creating a session or helper", async () => {
+ test("starts with the default model when no selection is available", async () => {
const f = fixture(true)
const { run: command } = await plugin(f)
f.selection.mockReturnValue(undefined)
- await command("gptlive.toggle")
- expect(f.toast).toHaveBeenCalledWith({
- title: "GPT-Live",
- message: "Choose a model before starting a voice call.",
- variant: "error",
- })
- expect(f.session.create).not.toHaveBeenCalled()
- expect(f.ensure).not.toHaveBeenCalled()
+ const starting = command("gptlive.toggle")
+ await Promise.resolve()
+ jest.advanceTimersByTime(25)
+ await starting
+ expect(f.session.create).toHaveBeenCalledWith({ location: f.location })
+ expect(f.session.switchModel).not.toHaveBeenCalled()
+ expect(f.ensure).toHaveBeenCalledTimes(1)
+ expect(f.rpc.start).toHaveBeenCalledWith(
+ { sessionID: "created", sdp: "offer", voice: undefined, fresh: false },
+ { location: f.location },
+ )
})
})
describe("voice startup cancellation", () => {
+ test("allows a retry before the cancelled model sync settles", async () => {
+ const f = fixture()
+ const reached = deferred()
+ const resume = deferred()
+ let syncs = 0
+ f.session.switchModel.mockImplementation(async () => {
+ syncs++
+ if (syncs === 1) {
+ reached.resolve()
+ await resume.promise
+ }
+ })
+
+ const first = f.controller.start("main", f.model)
+ await reached.promise
+ await f.controller.stop()
+ await f.controller.start("main", f.model)
+ expect(f.controller.state.phase).toBe("live")
+
+ resume.resolve()
+ await first
+ expect(f.controller.state.phase).toBe("live")
+ expect(f.rpc.start).toHaveBeenCalledTimes(1)
+ })
+
test.each(["model", "binary", "offer", "call", "answer"] as const)(
"stop invalidates an attempt waiting for %s",
async (stage) => {
@@ -295,6 +323,37 @@ describe("voice startup cancellation", () => {
})
describe("Home preparation cancellation", () => {
+ test("voice-new during session creation starts a fresh attempt", async () => {
+ const f = fixture(true)
+ const { run: command } = await plugin(f)
+ const firstCreated = deferred<{ id: string }>()
+ let creates = 0
+ f.session.create.mockImplementation(() => {
+ creates++
+ return creates === 1 ? firstCreated.promise : Promise.resolve({ id: "second" })
+ })
+
+ const first = command("gptlive.toggle")
+ await Promise.resolve()
+ const restart = command("gptlive.new")
+ await Promise.resolve()
+ await Promise.resolve()
+ jest.advanceTimersByTime(25)
+ await restart
+ expect(f.session.create).toHaveBeenCalledTimes(2)
+ expect(f.session.switchModel).toHaveBeenCalledWith({ sessionID: "second", model: f.model })
+ expect(f.rpc.start).toHaveBeenCalledTimes(1)
+ expect(f.rpc.start).toHaveBeenCalledWith(
+ { sessionID: "second", sdp: "offer", voice: "cove", fresh: true },
+ { location: f.location },
+ )
+
+ firstCreated.resolve({ id: "first" })
+ await first
+ expect(f.navigate).toHaveBeenCalledTimes(1)
+ expect(f.navigate).toHaveBeenCalledWith({ type: "session", sessionID: "second" })
+ })
+
test.each(["stop", "dispose"] as const)("%s during creation prevents late navigation and startup", async (action) => {
const f = fixture(true)
const { run: command, dispose } = await plugin(f)
diff --git a/test/model.test.ts b/test/model.test.ts
index 2c81a68..cace7eb 100644
--- a/test/model.test.ts
+++ b/test/model.test.ts
@@ -33,9 +33,10 @@ describe("voice coding-session model", () => {
{ sessionID: "coding-session", model: { providerID: "openai", id: "reasoner", variant: "high" } },
])
})
- test("does not fall back to another provider when no model is selected", async () => {
+ test("returns no model when the composer has no selection", async () => {
const { context, calls } = fixture()
- expect(() => callModel(context)).toThrow("Choose a model")
+ expect(callModel(context)).toBeUndefined()
+ await syncCallModel(context, "coding-session", undefined)
expect(calls).toEqual([])
})
test("awaits persistence and propagates failures before a call can start", async () => {