diff --git a/.github/release/lib.sh b/.github/release/lib.sh index 76ec08e6f308..8f058e233ed3 100644 --- a/.github/release/lib.sh +++ b/.github/release/lib.sh @@ -6,6 +6,11 @@ # environment the workflow sets (OPENCODE_VERSION, OPENCODE_CHANNEL, OPENCODE_DRY_RUN, ...). set -euo pipefail +# Without this, set -e does not apply inside $(...), so a failing command in a function called as +# files=$(upload_files ...) is ignored. macOS /bin/bash is 3.2, which lacks the option. +if [ "${BASH_VERSINFO[0]}" -gt 4 ] || { [ "${BASH_VERSINFO[0]}" = 4 ] && [ "${BASH_VERSINFO[1]}" -ge 4 ]; }; then + shopt -s inherit_errexit +fi # shellcheck disable=SC2034 # used by the scripts that source this file release_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) @@ -51,10 +56,7 @@ upload_files() { r2_credentials credentials=true fi - aws s3 cp "$file" "s3://opencode-production-files/bin/$OPENCODE_VERSION/$name" --only-show-errors \ - --endpoint-url "https://$CLOUDFLARE_ACCOUNT_ID.r2.cloudflarestorage.com" \ - --content-type "$type" --content-disposition "attachment; filename=\"$name\"" \ - --cache-control "public, max-age=31536000, immutable" >&2 + upload_file "$file" "$name" "$type" || die "Upload failed: $name" fi jq -n --arg name "$name" --arg url "https://opencode.ai/files/bin/$OPENCODE_VERSION/$name" \ --arg sha256 "$(sha256 "$file" | cut -c1-64)" --argjson size "$(size "$file")" \ @@ -62,15 +64,38 @@ upload_files() { done | jq -s 'add' } +# Uploads one file to R2 and confirms the stored object has the local size: upload_file . +# R2 intermittently rejects CompleteMultipartUpload with InvalidPart, so failed uploads are retried. +upload_file() { + local key="bin/$OPENCODE_VERSION/$2" endpoint="https://$CLOUDFLARE_ACCOUNT_ID.r2.cloudflarestorage.com" attempt + for attempt in 1 2 3; do + if aws s3 cp "$1" "s3://opencode-production-files/$key" --only-show-errors --endpoint-url "$endpoint" \ + --content-type "$3" --content-disposition "attachment; filename=\"$2\"" \ + --cache-control "public, max-age=31536000, immutable" >&2 && + [ "$(aws s3api head-object --bucket opencode-production-files --key "$key" --endpoint-url "$endpoint" \ + --query ContentLength --output text)" = "$(size "$1")" ]; then + return + fi + echo "::warning::Upload attempt $attempt failed: $2" >&2 + sleep $((attempt * 5)) + done + return 1 +} + # R2 accepts the API token ID as the access key and SHA-256(token) as the secret. r2_credentials() { local id id=$(curl -fsS -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ - "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/tokens/verify" | jq -er .result.id) + "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/tokens/verify" | jq -er .result.id) || + die "Could not resolve R2 credentials" export AWS_ACCESS_KEY_ID="$id" AWS_SECRET_ACCESS_KEY=$(printf '%s' "$CLOUDFLARE_API_TOKEN" | sha256 | cut -c1-64) export AWS_SECRET_ACCESS_KEY export AWS_DEFAULT_REGION=auto AWS_REQUEST_CHECKSUM_CALCULATION=when_required AWS_RESPONSE_CHECKSUM_VALIDATION=when_required + # Larger parts mean fewer CompleteMultipartUpload parts for the 200-350 MB desktop builds. + AWS_CONFIG_FILE=$(mktemp) + export AWS_CONFIG_FILE + printf '[default]\ns3 =\n multipart_threshold = 64MB\n multipart_chunksize = 64MB\n' > "$AWS_CONFIG_FILE" } # Registers one artifact with the update server: publish_update . diff --git a/CHANGELOG.md b/CHANGELOG.md index 62b870f15e30..b54ecd74ccc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,50 @@ Release notes for OpenCode V2, newest first. +## v2.2.5 — 2026-10-30 + +### Highlights + +- **Worktree and Shell Mode Shortcuts**: Press `⌘⌥O` on macOS or `Ctrl+Alt+O` on Windows and Linux to choose a worktree, and `⌘⌥B` or `Ctrl+Alt+B` to choose its base branch. `⌘⇧X` or `Ctrl+Shift+X` now toggles shell mode in the composer. ([#60713](https://github.com/anomalyco/opencode/pull/60713)) +- **Send Quotes From the Comment Popup**: Hold `⌘` or `Ctrl` in the quote popup to turn its checkmark into send, or press `⌘Enter` or `Ctrl+Enter`, to send just that quote and comment while leaving the rest of the composer untouched. ([#60640](https://github.com/anomalyco/opencode/pull/60640)) +- **Percentage Compaction Buffer**: Set `"compaction": { "buffer": "20%" }` in `opencode.json` to keep a share of each model's context window free, so one setting fits models with small and large windows. Token counts still work. ([#60647](https://github.com/anomalyco/opencode/pull/60647)) + +### Core + +#### Improvements + +- `compaction.buffer` accepts a percentage of the model's context window, such as `"20%"`, in addition to a token count. ([#60647](https://github.com/anomalyco/opencode/pull/60647)) +- When `read` misses because a folder is missing, the error names that folder and the nearest one that exists. ([#60688](https://github.com/anomalyco/opencode/pull/60688)) +- When `edit` can't find `oldString`, the error shows the file's closest lines, or says the change is already there. ([#60689](https://github.com/anomalyco/opencode/pull/60689)) +- Calls to a tool named `bash` now run as `shell` instead of failing, with the same permissions. A plugin tool actually named `bash` still takes precedence. ([#60687](https://github.com/anomalyco/opencode/pull/60687)) + +#### Bug fixes + +- MCP servers that send stdio messages larger than 10 MiB no longer get disconnected, and large messages are read much faster. ([#60694](https://github.com/anomalyco/opencode/pull/60694)) +- Idle locations no longer restart their MCP servers right after shutting down. ([#60668](https://github.com/anomalyco/opencode/pull/60668)) +- Compaction now caps its prompt at the model's context window when the input limit is larger. ([#60660](https://github.com/anomalyco/opencode/pull/60660)) +- LiteLLM context window errors now trigger compaction instead of stopping the session. ([#60692](https://github.com/anomalyco/opencode/pull/60692)) +- Reasoning is no longer turned into plain text when you switch models or resume an interrupted response, which caused request errors on some providers. ([#60498](https://github.com/anomalyco/opencode/pull/60498)) + +### TUI + +#### Bug fixes + +- The connect menu no longer lists MCP servers, which are managed in their own place. ([#60675](https://github.com/anomalyco/opencode/pull/60675)) + +### Desktop + +#### Improvements + +- Added keyboard shortcuts to choose a worktree and its base branch, shown in tooltips, and a shortcut to toggle shell mode. The worktree path can be selected to copy the full directory. ([#60713](https://github.com/anomalyco/opencode/pull/60713)) +- Hold `⌘` or `Ctrl` in the quote popup to send a quote and its comment directly. ([#60640](https://github.com/anomalyco/opencode/pull/60640)) +- Added flat input surfaces and a compact input size. ([#60598](https://github.com/anomalyco/opencode/pull/60598)) + +#### Bug fixes + +- The connect menu no longer lists MCP servers. ([#60675](https://github.com/anomalyco/opencode/pull/60675)) +- Added the missing top inset to the search field in the recently closed menu. ([#60652](https://github.com/anomalyco/opencode/pull/60652)) + ## v2.2.4 — 2026-10-30 ### Highlights diff --git a/bun.lock b/bun.lock index 3e4662d8e133..35cd72f18ffe 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/ai": { "name": "@opencode/ai", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "workspace:*", @@ -66,7 +66,7 @@ }, "packages/app": { "name": "@opencode/app", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -135,7 +135,7 @@ }, "packages/cli": { "name": "shuvcode", - "version": "2.2.4-shuv.1", + "version": "2.2.5-shuv.1", "bin": { "shuvcode": "./bin/shuvcode.mjs", }, @@ -199,7 +199,7 @@ }, "packages/client": { "name": "@opencode/client", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@opencode/protocol": "workspace:*", "@opencode/schema": "workspace:*", @@ -225,7 +225,7 @@ }, "packages/codemode": { "name": "@opencode/codemode", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -238,7 +238,7 @@ }, "packages/console/app": { "name": "@opencode/console-app", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@ibm/plex": "6.4.1", "@kobalte/core": "catalog:", @@ -276,7 +276,7 @@ }, "packages/core": { "name": "@opencode/core", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@ai-sdk/provider": "3.0.19", "@ai-sdk/provider-utils": "4.0.62", @@ -347,7 +347,7 @@ }, "packages/desktop": { "name": "@opencode/desktop", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@zip.js/zip.js": "2.7.62", "electron-context-menu": "5.0.0", @@ -395,7 +395,7 @@ }, "packages/function": { "name": "@opencode/function", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -429,7 +429,7 @@ }, "packages/gui-plugins": { "name": "@opencode/gui-plugins", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@betteroffice/docx": "0.4.3", "@betteroffice/fonts": "0.4.0", @@ -474,7 +474,7 @@ }, "packages/http-recorder": { "name": "@opencode/http-recorder", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@effect/platform-node-shared": "catalog:", }, @@ -493,7 +493,7 @@ }, "packages/httpapi-codegen": { "name": "@opencode/httpapi-codegen", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "effect": "catalog:", "prettier": "3.6.2", @@ -506,7 +506,7 @@ }, "packages/latex": { "name": "@opencode/latex", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@opencode/plugin": "workspace:*", "@opentui/core": "catalog:", @@ -520,7 +520,7 @@ }, "packages/merman": { "name": "@opencode/merman", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@opencode/plugin": "workspace:*", "@opentui/core": "catalog:", @@ -535,7 +535,7 @@ }, "packages/patchmark": { "name": "@opencode/patchmark", - "version": "2.2.4", + "version": "2.2.5", "devDependencies": { "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", @@ -548,7 +548,7 @@ }, "packages/plugin": { "name": "@opencode/plugin", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "workspace:*", @@ -593,7 +593,7 @@ }, "packages/plugin-browser": { "name": "@opencode/plugin-browser", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@opencode/plugin": "workspace:*", "@opencode/schema": "workspace:*", @@ -608,7 +608,7 @@ }, "packages/plugins/figma": { "name": "@opencode/plugin-figma", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "gray-matter": "4.0.3", }, @@ -638,7 +638,7 @@ }, "packages/protocol": { "name": "@opencode/protocol", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@opencode/schema": "workspace:*", "effect": "catalog:", @@ -668,7 +668,7 @@ }, "packages/schema": { "name": "@opencode/schema", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@standard-schema/spec": "catalog:", "effect": "catalog:", @@ -693,7 +693,7 @@ }, "packages/sdk": { "name": "@opencode/sdk", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@opencode/client": "workspace:*", "@opencode/core": "workspace:*", @@ -714,7 +714,7 @@ }, "packages/server": { "name": "@opencode/server", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@effect/platform-node": "catalog:", "@effect/platform-node-shared": "catalog:", @@ -737,7 +737,7 @@ }, "packages/session-ui": { "name": "@opencode/session-ui", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@kitlangton/solid-motion": "0.2.2", "@kobalte/core": "catalog:", @@ -769,7 +769,7 @@ }, "packages/simulation": { "name": "@opencode/simulation", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@opencode/ai": "workspace:*", "@opencode/core": "workspace:*", @@ -789,7 +789,7 @@ }, "packages/stats/app": { "name": "@opencode/stats-app", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@ibm/plex": "6.4.1", "@kobalte/core": "catalog:", @@ -815,7 +815,7 @@ }, "packages/stats/core": { "name": "@opencode/stats-core", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@planetscale/database": "1.19.0", "drizzle-orm": "catalog:", @@ -833,7 +833,7 @@ }, "packages/stats/server": { "name": "@opencode/stats-server", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@effect/platform-node": "catalog:", "@opencode/stats-core": "workspace:*", @@ -868,7 +868,7 @@ }, "packages/theme": { "name": "@opencode/theme", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@opentui/core": "catalog:", "effect": "catalog:", @@ -882,7 +882,7 @@ }, "packages/tui": { "name": "@opencode/tui", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@opencode/client": "workspace:*", "@opencode/core": "workspace:*", @@ -918,7 +918,7 @@ }, "packages/ui": { "name": "@opencode/ui", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@kitlangton/solid-motion": "0.2.2", "@kobalte/core": "catalog:", @@ -953,7 +953,7 @@ }, "packages/util": { "name": "@opencode/util", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", @@ -992,7 +992,7 @@ }, "packages/web": { "name": "@opencode/web", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "@astrojs/cloudflare": "14.1.4", "@astrojs/markdown-remark": "7.3.2", @@ -1044,7 +1044,7 @@ }, "services/update": { "name": "@opencode/update", - "version": "2.2.4", + "version": "2.2.5", "dependencies": { "jose": "6.0.11", "semver": "catalog:", @@ -1083,6 +1083,7 @@ "patchedDependencies": { "@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch", "@tanstack/virtual-core@3.17.8": "patches/@tanstack%2Fvirtual-core@3.17.8.patch", + "@brendonovich/vite-plugin-opencode@0.1.3": "patches/@brendonovich%2Fvite-plugin-opencode@0.1.3.patch", "electron-updater@6.8.9": "patches/electron-updater@6.8.9.patch", "pacote@21.5.1": "patches/pacote@21.5.1.patch", "@opentunnel/protocol@0.6.1": "patches/@opentunnel%2Fprotocol@0.6.1.patch", @@ -1196,13 +1197,13 @@ "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.301", "", { "os": "darwin", "cpu": "x64" }, "sha512-k6jNdu9eEhkPLTPTwHTn3rbIpKHm0mcndkS4wAZtQehCIFc69tmZZ4yeEtQ4eZ2jR0+6ttLtRy+5Q82Q6Ohm0Q=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.301", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-Uqjy+YD6ImvelrFs7WHRMboT3BqknGLrDSdiejDqzFk62TZZOqIH9OFHyiyvHexoO31CpDEcn+d7LB+SgpNurA=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.301", "", { "os": "linux", "cpu": "arm64" }, "sha512-Uqjy+YD6ImvelrFs7WHRMboT3BqknGLrDSdiejDqzFk62TZZOqIH9OFHyiyvHexoO31CpDEcn+d7LB+SgpNurA=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.301", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-C1+q1cYZY2bYxDYCfHHaq9ToGMTO/6FdbB0JShyPCyAt0lrRC+myNgsvPq/0ZsrtPWbcbrpX6QgbsFn2QzEWcw=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.301", "", { "os": "linux", "cpu": "arm64" }, "sha512-C1+q1cYZY2bYxDYCfHHaq9ToGMTO/6FdbB0JShyPCyAt0lrRC+myNgsvPq/0ZsrtPWbcbrpX6QgbsFn2QzEWcw=="], - "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.301", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-V0zrxg59pBSp6PXPD70m/nRwUApKM/q5SJdeHq2ZAn7mcAr/GIBRoBEAIrA2kOAK9EkIyjKNKkrwuOVeyId/lA=="], + "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.301", "", { "os": "linux", "cpu": "x64" }, "sha512-V0zrxg59pBSp6PXPD70m/nRwUApKM/q5SJdeHq2ZAn7mcAr/GIBRoBEAIrA2kOAK9EkIyjKNKkrwuOVeyId/lA=="], - "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.301", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-qmHolcb+CgzYc4rfZrJd5iEIzOPRPz2edJBH+8V4W4R+bswsNaqf59NBkrDt3GdfmGr42touHYJ/4dV+g/b5eQ=="], + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.301", "", { "os": "linux", "cpu": "x64" }, "sha512-qmHolcb+CgzYc4rfZrJd5iEIzOPRPz2edJBH+8V4W4R+bswsNaqf59NBkrDt3GdfmGr42touHYJ/4dV+g/b5eQ=="], "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.301", "", { "os": "win32", "cpu": "arm64" }, "sha512-V/iwQ3/wcxeQaycw2Daym9CzxssjYXIWtQJEFoU+Zrck3++2lD24KxFK0pwB3f258/uwdKoAM0x5p7+FEMRzwQ=="], @@ -1246,13 +1247,13 @@ "@astrojs/compiler-binding-darwin-x64": ["@astrojs/compiler-binding-darwin-x64@0.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-+kl35S7hiHJ2cb7eujwEsPFYYuSgE11uEE2Z20lAJ+undpgulMcW+weQqFPOSSZccEKHOiLIY+kQHkb4e4092w=="], - "@astrojs/compiler-binding-linux-arm64-gnu": ["@astrojs/compiler-binding-linux-arm64-gnu@0.5.2", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-BqlR0EHNQLPLwgL9+xogjsAc7zGf0cHho2CPdhv0P1yqbVMwGVaGAqrANP6VYAXCqSC43AF4Czcxc2n+oqdesg=="], + "@astrojs/compiler-binding-linux-arm64-gnu": ["@astrojs/compiler-binding-linux-arm64-gnu@0.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-BqlR0EHNQLPLwgL9+xogjsAc7zGf0cHho2CPdhv0P1yqbVMwGVaGAqrANP6VYAXCqSC43AF4Czcxc2n+oqdesg=="], - "@astrojs/compiler-binding-linux-arm64-musl": ["@astrojs/compiler-binding-linux-arm64-musl@0.5.2", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-GRGyby3AZWw5d6JQzidyxPyvGgHWsSusrpgKyFVpDTKctABF7L3Pd5V1cRJbV36/rcu+Eh292/wL7pF7MkmGkQ=="], + "@astrojs/compiler-binding-linux-arm64-musl": ["@astrojs/compiler-binding-linux-arm64-musl@0.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-GRGyby3AZWw5d6JQzidyxPyvGgHWsSusrpgKyFVpDTKctABF7L3Pd5V1cRJbV36/rcu+Eh292/wL7pF7MkmGkQ=="], - "@astrojs/compiler-binding-linux-x64-gnu": ["@astrojs/compiler-binding-linux-x64-gnu@0.5.2", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-9XAqGTMQAtIslMsjK3xdyY6OI9d59OZqcLBB6pvD/DcoR0daYswfrKx2azdDERDY4AqAnWPC7odVM1BmxYv3Vw=="], + "@astrojs/compiler-binding-linux-x64-gnu": ["@astrojs/compiler-binding-linux-x64-gnu@0.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-9XAqGTMQAtIslMsjK3xdyY6OI9d59OZqcLBB6pvD/DcoR0daYswfrKx2azdDERDY4AqAnWPC7odVM1BmxYv3Vw=="], - "@astrojs/compiler-binding-linux-x64-musl": ["@astrojs/compiler-binding-linux-x64-musl@0.5.2", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-jOViewGeTh9bSfS8vK72ECxoy2yvmA+Pd/ZcEvBI5c7+7IcfHwZ5qA4L9fRYuOhSrAifvQ2dV4vm1XAtloaWBw=="], + "@astrojs/compiler-binding-linux-x64-musl": ["@astrojs/compiler-binding-linux-x64-musl@0.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-jOViewGeTh9bSfS8vK72ECxoy2yvmA+Pd/ZcEvBI5c7+7IcfHwZ5qA4L9fRYuOhSrAifvQ2dV4vm1XAtloaWBw=="], "@astrojs/compiler-binding-wasm32-wasi": ["@astrojs/compiler-binding-wasm32-wasi@0.5.2", "", { "dependencies": { "@emnapi/core": "1.11.3", "@emnapi/runtime": "1.11.3", "@napi-rs/wasm-runtime": "^1.2.5" }, "cpu": "none" }, "sha512-Z7p64doar1uvR5C9Xs6j1CV9lGlPVQX7OQ2aAtZ/xKC5aGS6GB13nyc2UfQXTQLvZrvVwsyaflTQy6ziY5Bekw=="], @@ -1542,13 +1543,13 @@ "@bruits/satteri-darwin-x64": ["@bruits/satteri-darwin-x64@0.9.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-6T26Z5Kf3cFW2PSlk9p7zT7yVxvuBSiJvYyz9u8KjYwMTqZyIDOj2wDyNpxKV4+6yUVG7rddq2QwvG/8LJA2+Q=="], - "@bruits/satteri-linux-arm64-gnu": ["@bruits/satteri-linux-arm64-gnu@0.9.5", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-u51id17uJwNEMK9nBlICsq6U31c+XVqQueVBkwRIzZG+gMpS8TOJctt5h5Wz33Z8xnMdTd+adtACVz0yHgGuOA=="], + "@bruits/satteri-linux-arm64-gnu": ["@bruits/satteri-linux-arm64-gnu@0.9.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-u51id17uJwNEMK9nBlICsq6U31c+XVqQueVBkwRIzZG+gMpS8TOJctt5h5Wz33Z8xnMdTd+adtACVz0yHgGuOA=="], - "@bruits/satteri-linux-arm64-musl": ["@bruits/satteri-linux-arm64-musl@0.9.5", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-v39HxiwGC5Rqm01HksP6+5Y+xKLPlsuVFgIgpEAo+SiQ22c+mJVhS3u7Z6ePAKdhL5NJoK1xq70kLz3L13AhpQ=="], + "@bruits/satteri-linux-arm64-musl": ["@bruits/satteri-linux-arm64-musl@0.9.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-v39HxiwGC5Rqm01HksP6+5Y+xKLPlsuVFgIgpEAo+SiQ22c+mJVhS3u7Z6ePAKdhL5NJoK1xq70kLz3L13AhpQ=="], - "@bruits/satteri-linux-x64-gnu": ["@bruits/satteri-linux-x64-gnu@0.9.5", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-F3uO8uFp3pAP5ZGXttwvh57GS7s0lL953tnNdyI2gRyP4kOOkp6pyGojNJzCjkDvWI2Cvb9iNrKok3aqQPauAw=="], + "@bruits/satteri-linux-x64-gnu": ["@bruits/satteri-linux-x64-gnu@0.9.5", "", { "os": "linux", "cpu": "x64" }, "sha512-F3uO8uFp3pAP5ZGXttwvh57GS7s0lL953tnNdyI2gRyP4kOOkp6pyGojNJzCjkDvWI2Cvb9iNrKok3aqQPauAw=="], - "@bruits/satteri-linux-x64-musl": ["@bruits/satteri-linux-x64-musl@0.9.5", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-bicEqglLlz++mWyADaZoP0JY20s4vDfLjaPYgQqC+NI4zZLTOOg1T4GB8aqtc822Pqji8SQBmSrTb7CrP8i08Q=="], + "@bruits/satteri-linux-x64-musl": ["@bruits/satteri-linux-x64-musl@0.9.5", "", { "os": "linux", "cpu": "x64" }, "sha512-bicEqglLlz++mWyADaZoP0JY20s4vDfLjaPYgQqC+NI4zZLTOOg1T4GB8aqtc822Pqji8SQBmSrTb7CrP8i08Q=="], "@bruits/satteri-wasm32-wasi": ["@bruits/satteri-wasm32-wasi@0.9.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-zauAuMwfPnKPUkd4AFixRFpXdgKwP2mKgxrIIo2gJzW0/ZneF9dbHnLkojSpaBnCCp7VUL1hIi5WWZvB1CqmAQ=="], @@ -1716,11 +1717,11 @@ "@ff-labs/fff-bin-darwin-x64": ["@ff-labs/fff-bin-darwin-x64@0.10.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-pwdFJUB/oeO4S/SfmRDdE5ipbAMYoVtRMwTZ12Wn2H46PgNU5DWnSjHhUNTLuKmkQU/ZIJL5Sn9j/fzwuIzcoA=="], - "@ff-labs/fff-bin-linux-arm64-gnu": ["@ff-labs/fff-bin-linux-arm64-gnu@0.10.5", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-prbY72gR+VPoSxl1InglwSwVHM0kErYMjUOpdvjQhS8eRdXYL3hTuYQv9L7fiB0mqaDzcwFa339uMr3p8ujDrw=="], + "@ff-labs/fff-bin-linux-arm64-gnu": ["@ff-labs/fff-bin-linux-arm64-gnu@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-prbY72gR+VPoSxl1InglwSwVHM0kErYMjUOpdvjQhS8eRdXYL3hTuYQv9L7fiB0mqaDzcwFa339uMr3p8ujDrw=="], "@ff-labs/fff-bin-linux-arm64-musl": ["@ff-labs/fff-bin-linux-arm64-musl@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-MQN/CEKn4c4toooGCmZ6N8cP9mhgE4tnR6rr6a4k9JjH0xi05fYdG4+BabPkr+l/mDc01DsRpSKVNBmvoZ3IlQ=="], - "@ff-labs/fff-bin-linux-x64-gnu": ["@ff-labs/fff-bin-linux-x64-gnu@0.10.5", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-8NzFGGyqrFaCtb+SPTFd0xQGUlHCJMf0KYGwuca46IP37nN4OtDT1wnyZ9oaPDZ+2qqyxox10hCnqGlmj3PJzQ=="], + "@ff-labs/fff-bin-linux-x64-gnu": ["@ff-labs/fff-bin-linux-x64-gnu@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-8NzFGGyqrFaCtb+SPTFd0xQGUlHCJMf0KYGwuca46IP37nN4OtDT1wnyZ9oaPDZ+2qqyxox10hCnqGlmj3PJzQ=="], "@ff-labs/fff-bin-linux-x64-musl": ["@ff-labs/fff-bin-linux-x64-musl@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mioRbn8aAW2Jmm4W0w3poePo65iSE+REfO+Jc0iqsjirImEy6pKt7dswj+7LSU6RPWwPsMb4POTMENwNLMlJww=="], @@ -1776,37 +1777,37 @@ "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ=="], - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.4", "", { "os": "linux", "cpu": "arm", "libc": "glibc" }, "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.4", "", { "os": "linux", "cpu": "arm" }, "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw=="], - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A=="], - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.4", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ=="], - "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.4", "", { "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q=="], + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.4", "", { "os": "linux", "cpu": "none" }, "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.4", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.4", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.4", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg=="], - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.4" }, "os": "linux", "cpu": "arm", "libc": "glibc" }, "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ=="], + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.4" }, "os": "linux", "cpu": "arm" }, "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ=="], - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ=="], + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ=="], - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.4" }, "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA=="], - "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.4" }, "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ=="], + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.4" }, "os": "linux", "cpu": "none" }, "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ=="], - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.4" }, "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.4" }, "os": "linux", "cpu": "s390x" }, "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA=="], - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.4" }, "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog=="], + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog=="], - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg=="], + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg=="], - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.4" }, "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A=="], + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A=="], "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], @@ -2012,13 +2013,13 @@ "@opencode/pty-darwin-x64": ["@opencode/pty-darwin-x64@0.4.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-NvQfygGcsGmDCIUJ/VHLcS6SduC93Y/qP2hrHjtBpiL2sSQyeCDcifNhLvqY/wxGFNNZ4GeZwwZqe7rMEuBJbQ=="], - "@opencode/pty-linux-arm64-gnu": ["@opencode/pty-linux-arm64-gnu@0.4.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-VZXkYD+CT+zdodmTnKn9/VlmswksLPBLJryeBsG4IXUFXTsATr7+PVygJk96FxCh5nq68H3a/UJgyiIUMl1XCQ=="], + "@opencode/pty-linux-arm64-gnu": ["@opencode/pty-linux-arm64-gnu@0.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-VZXkYD+CT+zdodmTnKn9/VlmswksLPBLJryeBsG4IXUFXTsATr7+PVygJk96FxCh5nq68H3a/UJgyiIUMl1XCQ=="], - "@opencode/pty-linux-arm64-musl": ["@opencode/pty-linux-arm64-musl@0.4.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-hCx25Hn9sALdnkx2Y4aPGVw3glTbX61XRiJs+XETPPIO9QTaav4OS4k2JR2uHC6/u161xKdN82iJ+OYfwcGUSg=="], + "@opencode/pty-linux-arm64-musl": ["@opencode/pty-linux-arm64-musl@0.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-hCx25Hn9sALdnkx2Y4aPGVw3glTbX61XRiJs+XETPPIO9QTaav4OS4k2JR2uHC6/u161xKdN82iJ+OYfwcGUSg=="], - "@opencode/pty-linux-x64-gnu": ["@opencode/pty-linux-x64-gnu@0.4.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-Xal/d0+xp6XE4iCGUAw4JTQ4uf5l8V0EIbeKq+e03cHJewOMKZk4fooQjxpZvQIToBHRtnCJAmd+EEhVxXiDDw=="], + "@opencode/pty-linux-x64-gnu": ["@opencode/pty-linux-x64-gnu@0.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Xal/d0+xp6XE4iCGUAw4JTQ4uf5l8V0EIbeKq+e03cHJewOMKZk4fooQjxpZvQIToBHRtnCJAmd+EEhVxXiDDw=="], - "@opencode/pty-linux-x64-musl": ["@opencode/pty-linux-x64-musl@0.4.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-dBVtKZoVKAN6/E+65GrJksgRKzPkPHUjWZBzf3mRANXt2adpdbV+/M1AMfPCZSju2mN1j9rNu8MCrxwr+ugEig=="], + "@opencode/pty-linux-x64-musl": ["@opencode/pty-linux-x64-musl@0.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-dBVtKZoVKAN6/E+65GrJksgRKzPkPHUjWZBzf3mRANXt2adpdbV+/M1AMfPCZSju2mN1j9rNu8MCrxwr+ugEig=="], "@opencode/pty-win32-arm64": ["@opencode/pty-win32-arm64@0.4.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-IKJMUnS2aluWqRj13enMWomYtwbXq8rStcf/mUbxBoAGTDMtWR/ZrRmKb2ptR7KUqDjdSEa+8C8rczS7uGFl9g=="], @@ -2096,11 +2097,11 @@ "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.8.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-b6D/VP9y0Z18tO86GSTp9BBykgR6r5CQM1ol9UMJfUt+PjN5HGksbOCDTMIQufXvt4WIUFHrSBProysJRFzbYw=="], - "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.8.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-8ZPl/2DChcS7XIVRPIazigXaMNyxa2bwvuzPprNbzT0+cVJLIoGC19pjiV3bI3oVXBPVMNqgIcppOrEHBhlouw=="], + "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.8.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-8ZPl/2DChcS7XIVRPIazigXaMNyxa2bwvuzPprNbzT0+cVJLIoGC19pjiV3bI3oVXBPVMNqgIcppOrEHBhlouw=="], "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.8.0", "", { "os": "linux", "cpu": "x64" }, "sha512-aXBNIdE6iMf58/Tdnd6UzXeFjYfaHzQ6E0nukjLe2ir41/meJJYEAHUQ62EBr/gxvuCNZHTBQB4T9BzF7PdENQ=="], - "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.8.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-xGEYw6wkX0sPwPx6RosQZanuWF0+5cIup2alhHHQQfKJkcLS8r8NBWlcy2LK9CoEWH6SBtNXC+9Y5IrryCOvJQ=="], + "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.8.0", "", { "os": "linux", "cpu": "x64" }, "sha512-xGEYw6wkX0sPwPx6RosQZanuWF0+5cIup2alhHHQQfKJkcLS8r8NBWlcy2LK9CoEWH6SBtNXC+9Y5IrryCOvJQ=="], "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.8.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-XtK0QMyIiqeDmrIyyAXqqp7TKaRGaWnBlbnEsNrkRLrgP+aLwRhRSHdafxi+RMqj3h9qOlb1MgnPal1kx7wP4g=="], @@ -2130,21 +2131,21 @@ "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.141.0", "", { "os": "linux", "cpu": "arm" }, "sha512-e6hwQqd+3lvP13G2jxvFpoA7dzHcFLN+Mq47JCVMtdNHbbyBRo756JCtbbJH6ca8inTfyqZoqBmS3vhQlzAK2w=="], - "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.141.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-vXz2BLAuypA+4MLyBg94pzEo6THVnzYnCtAjXoihIIQo0t2pnp/AmW+SH1EI+4VbuJnC//KplIJ5yyaCGua4jA=="], + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.141.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vXz2BLAuypA+4MLyBg94pzEo6THVnzYnCtAjXoihIIQo0t2pnp/AmW+SH1EI+4VbuJnC//KplIJ5yyaCGua4jA=="], - "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.141.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-jMkS/EztNW34HKsXIaT/SoHcmtocq/vWhwFOVduF9kduuuRIVwfwQ6uxzIO+qPKSXdd2TXt54of0BJ2zFMXnmw=="], + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.141.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jMkS/EztNW34HKsXIaT/SoHcmtocq/vWhwFOVduF9kduuuRIVwfwQ6uxzIO+qPKSXdd2TXt54of0BJ2zFMXnmw=="], - "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.141.0", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-vo+MR+n3zQJ6Mq92hiP084NZcgDv5iJlVR02gMf28neMvVT1tKVm7VeiW/DxhdqOi3QLeaXIk9cUcLL1qrkngw=="], + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.141.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vo+MR+n3zQJ6Mq92hiP084NZcgDv5iJlVR02gMf28neMvVT1tKVm7VeiW/DxhdqOi3QLeaXIk9cUcLL1qrkngw=="], - "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.141.0", "", { "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-oh80w+7RuiO5gBp9Jnoa/H8Qlt3JsHL2MkW+0dwEdlDMdslVZX/YsekSK6EeyEenY66/mhCfypsNATQ7Ph3qlQ=="], + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.141.0", "", { "os": "linux", "cpu": "none" }, "sha512-oh80w+7RuiO5gBp9Jnoa/H8Qlt3JsHL2MkW+0dwEdlDMdslVZX/YsekSK6EeyEenY66/mhCfypsNATQ7Ph3qlQ=="], - "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.141.0", "", { "os": "linux", "cpu": "none", "libc": "musl" }, "sha512-LOyEmFA8sCnYbEXP1+iQvCC/P1YXHMA/t6x1Ksp0Y9VwhLFsiBJFzV1zIxrOIE2LKaGGhDjQ29xq9cbq6omDXA=="], + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.141.0", "", { "os": "linux", "cpu": "none" }, "sha512-LOyEmFA8sCnYbEXP1+iQvCC/P1YXHMA/t6x1Ksp0Y9VwhLFsiBJFzV1zIxrOIE2LKaGGhDjQ29xq9cbq6omDXA=="], - "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.141.0", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-3wnwk/l1CvszVE5TJR1wSl/zSEfydRqrNhn6s7Vr9IzSJpUQIroqVsIoPARHRFA+FQwkxAFDAHDAasa7v8OobQ=="], + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.141.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-3wnwk/l1CvszVE5TJR1wSl/zSEfydRqrNhn6s7Vr9IzSJpUQIroqVsIoPARHRFA+FQwkxAFDAHDAasa7v8OobQ=="], - "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.141.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-qtyQVAAebFq57B2tifTlel3TgGqUtsYNI/e+p6aya9rN9lOZVTDvr215fGYSA9XWooxzMxDiVxkBLk2jQHbsOQ=="], + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.141.0", "", { "os": "linux", "cpu": "x64" }, "sha512-qtyQVAAebFq57B2tifTlel3TgGqUtsYNI/e+p6aya9rN9lOZVTDvr215fGYSA9XWooxzMxDiVxkBLk2jQHbsOQ=="], - "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.141.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-SkGV1nKw40roEc94pv5EaaeH2ay14G6+roe8Q0wIUC1LcEKxzKW921h7+ZuZX0D3q2Mb/7aSFmxEVqnko3lPRw=="], + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.141.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SkGV1nKw40roEc94pv5EaaeH2ay14G6+roe8Q0wIUC1LcEKxzKW921h7+ZuZX0D3q2Mb/7aSFmxEVqnko3lPRw=="], "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.141.0", "", { "os": "none", "cpu": "arm64" }, "sha512-cVgDM7n8QziQqOaP5hNgUYfMG7S/ZeuPxFWXnnHRv7rh025COk0rfQ6eEdKG3j/GaUuyvNZN4ifF1J8KmuXLLA=="], @@ -2228,11 +2229,11 @@ "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="], - "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="], + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="], "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="], - "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="], + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="], "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="], @@ -2766,11 +2767,11 @@ "@yuuang/ffi-rs-linux-arm-gnueabihf": ["@yuuang/ffi-rs-linux-arm-gnueabihf@1.3.7", "", { "os": "linux", "cpu": "arm" }, "sha512-9r//Z022QYVoIAY458Dpk8HWkisk/y4NJC3d52RlkdsLD+9p0zn0cKKSoMF/xUsOWDFSmOf+cwU/Xadi/wxFMQ=="], - "@yuuang/ffi-rs-linux-arm64-gnu": ["@yuuang/ffi-rs-linux-arm64-gnu@1.3.2", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-gWFO6xufUK9lPYUqDvKa6IR243dPqdetgl9Q7HrZWaDu7wLo06QQrosw8QTzndafQnOcBKm6LoLujmGCfTgJOA=="], + "@yuuang/ffi-rs-linux-arm64-gnu": ["@yuuang/ffi-rs-linux-arm64-gnu@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-gWFO6xufUK9lPYUqDvKa6IR243dPqdetgl9Q7HrZWaDu7wLo06QQrosw8QTzndafQnOcBKm6LoLujmGCfTgJOA=="], "@yuuang/ffi-rs-linux-arm64-musl": ["@yuuang/ffi-rs-linux-arm64-musl@1.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-eLLx4P8DzNnuPif19nAvKPaf88lSf6KFbDetH3nAIRZvI495h0jigN835Ayr7uAADssohGfO9MPMH33DXmFErA=="], - "@yuuang/ffi-rs-linux-x64-gnu": ["@yuuang/ffi-rs-linux-x64-gnu@1.3.2", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-s8VCFazaJKmgY2hgMTpWk4TtBY/zy5ovbaGgwyY0FvBD0YvyhcET4IrMsDJpHhFVTPCYfKZ1dN45clD/YiFp6g=="], + "@yuuang/ffi-rs-linux-x64-gnu": ["@yuuang/ffi-rs-linux-x64-gnu@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-s8VCFazaJKmgY2hgMTpWk4TtBY/zy5ovbaGgwyY0FvBD0YvyhcET4IrMsDJpHhFVTPCYfKZ1dN45clD/YiFp6g=="], "@yuuang/ffi-rs-linux-x64-musl": ["@yuuang/ffi-rs-linux-x64-musl@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-YOcOkIwVvIhpwuGJ428h15vpE0KnT3teFG0g2J31FrBxrVh9IH4HeTGXjelI3l9SI5gyiZDnD4PQnQ1U6oqTcA=="], @@ -5448,13 +5449,13 @@ "@astrojs/markdown-satteri/satteri/@bruits/satteri-darwin-x64": ["@bruits/satteri-darwin-x64@0.10.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q=="], - "@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-arm64-gnu": ["@bruits/satteri-linux-arm64-gnu@0.10.5", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w=="], + "@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-arm64-gnu": ["@bruits/satteri-linux-arm64-gnu@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w=="], - "@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-arm64-musl": ["@bruits/satteri-linux-arm64-musl@0.10.5", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA=="], + "@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-arm64-musl": ["@bruits/satteri-linux-arm64-musl@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA=="], - "@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-x64-gnu": ["@bruits/satteri-linux-x64-gnu@0.10.5", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw=="], + "@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-x64-gnu": ["@bruits/satteri-linux-x64-gnu@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw=="], - "@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-x64-musl": ["@bruits/satteri-linux-x64-musl@0.10.5", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg=="], + "@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-x64-musl": ["@bruits/satteri-linux-x64-musl@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg=="], "@astrojs/markdown-satteri/satteri/@bruits/satteri-wasm32-wasi": ["@bruits/satteri-wasm32-wasi@0.10.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.2.3" }, "cpu": "none" }, "sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw=="], @@ -5622,17 +5623,17 @@ "astro/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.14", "", { "os": "linux", "cpu": "arm" }, "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg=="], - "astro/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.14", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ=="], + "astro/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ=="], - "astro/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.14", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg=="], + "astro/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg=="], - "astro/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.14", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q=="], + "astro/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.14", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q=="], - "astro/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.14", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A=="], + "astro/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.14", "", { "os": "linux", "cpu": "s390x" }, "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A=="], - "astro/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.14", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg=="], + "astro/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.14", "", { "os": "linux", "cpu": "x64" }, "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg=="], - "astro/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.14", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg=="], + "astro/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.14", "", { "os": "linux", "cpu": "x64" }, "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg=="], "astro/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.14", "", { "os": "none", "cpu": "arm64" }, "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q=="], @@ -5752,17 +5753,17 @@ "nitro/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.14", "", { "os": "linux", "cpu": "arm" }, "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg=="], - "nitro/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.14", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ=="], + "nitro/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ=="], - "nitro/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.14", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg=="], + "nitro/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg=="], - "nitro/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.14", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q=="], + "nitro/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.14", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q=="], - "nitro/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.14", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A=="], + "nitro/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.14", "", { "os": "linux", "cpu": "s390x" }, "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A=="], - "nitro/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.14", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg=="], + "nitro/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.14", "", { "os": "linux", "cpu": "x64" }, "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg=="], - "nitro/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.14", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg=="], + "nitro/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.14", "", { "os": "linux", "cpu": "x64" }, "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg=="], "nitro/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.14", "", { "os": "none", "cpu": "arm64" }, "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q=="], @@ -5878,17 +5879,17 @@ "@astrojs/solid-js/vite/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.14", "", { "os": "linux", "cpu": "arm" }, "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg=="], - "@astrojs/solid-js/vite/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.14", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ=="], + "@astrojs/solid-js/vite/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ=="], - "@astrojs/solid-js/vite/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.14", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg=="], + "@astrojs/solid-js/vite/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg=="], - "@astrojs/solid-js/vite/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.14", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q=="], + "@astrojs/solid-js/vite/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.14", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q=="], - "@astrojs/solid-js/vite/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.14", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A=="], + "@astrojs/solid-js/vite/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.14", "", { "os": "linux", "cpu": "s390x" }, "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A=="], - "@astrojs/solid-js/vite/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.14", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg=="], + "@astrojs/solid-js/vite/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.14", "", { "os": "linux", "cpu": "x64" }, "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg=="], - "@astrojs/solid-js/vite/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.14", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg=="], + "@astrojs/solid-js/vite/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.14", "", { "os": "linux", "cpu": "x64" }, "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg=="], "@astrojs/solid-js/vite/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.14", "", { "os": "none", "cpu": "arm64" }, "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q=="], @@ -5902,13 +5903,13 @@ "@astrojs/starlight/@astrojs/markdown-satteri/satteri/@bruits/satteri-darwin-x64": ["@bruits/satteri-darwin-x64@0.10.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q=="], - "@astrojs/starlight/@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-arm64-gnu": ["@bruits/satteri-linux-arm64-gnu@0.10.5", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w=="], + "@astrojs/starlight/@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-arm64-gnu": ["@bruits/satteri-linux-arm64-gnu@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w=="], - "@astrojs/starlight/@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-arm64-musl": ["@bruits/satteri-linux-arm64-musl@0.10.5", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA=="], + "@astrojs/starlight/@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-arm64-musl": ["@bruits/satteri-linux-arm64-musl@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA=="], - "@astrojs/starlight/@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-x64-gnu": ["@bruits/satteri-linux-x64-gnu@0.10.5", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw=="], + "@astrojs/starlight/@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-x64-gnu": ["@bruits/satteri-linux-x64-gnu@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw=="], - "@astrojs/starlight/@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-x64-musl": ["@bruits/satteri-linux-x64-musl@0.10.5", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg=="], + "@astrojs/starlight/@astrojs/markdown-satteri/satteri/@bruits/satteri-linux-x64-musl": ["@bruits/satteri-linux-x64-musl@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg=="], "@astrojs/starlight/@astrojs/markdown-satteri/satteri/@bruits/satteri-wasm32-wasi": ["@bruits/satteri-wasm32-wasi@0.10.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.2.3" }, "cpu": "none" }, "sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw=="], diff --git a/package.json b/package.json index 03a0fb46c051..9f103c577e2a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/package.json", "name": "opencode", "description": "AI-powered development tool", - "version": "2.2.4", + "version": "2.2.5", "private": true, "type": "module", "packageManager": "bun@1.4.4", @@ -183,6 +183,7 @@ "electron-updater@6.8.9": "patches/electron-updater@6.8.9.patch", "@pierre/trees@1.0.0-beta.6": "patches/@pierre%2Ftrees@1.0.0-beta.6.patch", "@opentunnel/client@0.6.1": "patches/@opentunnel%2Fclient@0.6.1.patch", - "@opentunnel/protocol@0.6.1": "patches/@opentunnel%2Fprotocol@0.6.1.patch" + "@opentunnel/protocol@0.6.1": "patches/@opentunnel%2Fprotocol@0.6.1.patch", + "@brendonovich/vite-plugin-opencode@0.1.3": "patches/@brendonovich%2Fvite-plugin-opencode@0.1.3.patch" } } diff --git a/packages/ai/package.json b/packages/ai/package.json index c9354d71b25e..fd8bf980601d 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "2.2.4", + "version": "2.2.5", "name": "@opencode/ai", "type": "module", "license": "MIT", diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index 36eb59151acc..eb98402e373a 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -864,6 +864,7 @@ const requireThinkingSignature = (request: LLMRequest) => { ) return false if (provider.includes("xiaomi") || model.includes("mimo") || baseURL.includes("xiaomimimo.com")) return false + if (model.startsWith("qwen") || model.startsWith("minimax-")) return false return true } @@ -992,19 +993,9 @@ const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest, breakpoi continue } if (!signature || signature.trim().length === 0) { - if (part.text.trim().length === 0) continue - if (!requireThinkingSignature(request)) { + if (part.text.trim().length > 0 && !requireThinkingSignature(request)) { content.push({ type: "thinking", thinking: part.text, signature: "" }) - continue } - // Without a signature this cannot be a valid thinking block per - // the SDK ThinkingBlockParam:3217 — demote to text so the - // conversation remains sendable. - content.push({ - type: "text", - text: part.text, - cache_control: cacheControl(breakpoints, part.cache), - }) continue } content.push({ type: "thinking", thinking: part.text, signature }) diff --git a/packages/ai/src/protocols/bedrock-converse.ts b/packages/ai/src/protocols/bedrock-converse.ts index 8cdc5d897b06..758e7d8cf0ac 100644 --- a/packages/ai/src/protocols/bedrock-converse.ts +++ b/packages/ai/src/protocols/bedrock-converse.ts @@ -456,12 +456,7 @@ const lowerMessages = Effect.fnUntraced(function* ( content.push({ reasoningContent: { redactedContent: redactedData } }) continue } - if (signature === undefined || signature.trim().length === 0) { - // Interrupted streams and model switches can leave unsigned reasoning. - // Preserve readable history as text rather than replay invalid reasoningContent. - if (part.text.trim().length > 0) content.push(...textWithCache(breakpoints, part.text, part.cache)) - continue - } + if (signature === undefined || signature.trim().length === 0) continue content.push({ reasoningContent: { reasoningText: { text: part.text, signature } } }) continue } diff --git a/packages/ai/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts index ba5d67aa5ca8..327b42934f22 100644 --- a/packages/ai/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -32,22 +32,12 @@ import { Lifecycle } from "./utils/lifecycle.js" const ADAPTER = "gemini" const CONTINUATION = "CONTINUATION" -// Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost. +// Google documents this sentinel for replaying function calls after their original signature was lost. const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator" // Gemini 2.5 rejects a budget under the model's minimum: 512 on Flash-Lite, the highest, and 128 on Pro. const MIN_THINKING_BUDGET = 512 export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" -// Gemini 3 rejects replayed function calls without a thought signature. Google's SDKs avoid that in normal chats by -// retaining complete model responses, but OpenCode reconstructs durable history and may encounter an unsigned call -// from an older or external session. Model IDs are open-ended, so unknown Gemini aliases inherit current behavior. -const requiresThoughtSignatureFallback = (modelID: string) => { - if (!/(^|\/)gemini-/i.test(modelID)) return false - if (/(^|\/)gemini-(?:1|2)(?:[.-]|$)/i.test(modelID)) return false - if (/(^|\/)gemini-pro(?:-vision)?$/i.test(modelID)) return false - return !/(^|\/)gemini-robotics-er-1\.5(?:[.-]|$)/i.test(modelID) -} - // Gemini 3 accepts media nested inside function responses; matched Gemini 2.5 variants reject it, // so their tool-result attachments lower as a separate user turn instead. const routesLegacyToolMedia = (modelID: string) => /gemini-2[.-]5(?:[.-]|$)/i.test(modelID) @@ -390,11 +380,7 @@ const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest) { const signature = lowered.thoughtSignature parts.push({ ...lowered, - thoughtSignature: - signature ?? - (requiresThoughtSignatureFallback(request.model.id) && !hasSignedToolCall - ? SKIP_THOUGHT_SIGNATURE_VALIDATOR - : undefined), + thoughtSignature: signature ?? (!hasSignedToolCall ? SKIP_THOUGHT_SIGNATURE_VALIDATOR : undefined), }) if (signature !== undefined) hasSignedToolCall = true continue diff --git a/packages/ai/src/protocols/google-interactions.ts b/packages/ai/src/protocols/google-interactions.ts index a04083fd0783..24d2179afb19 100644 --- a/packages/ai/src/protocols/google-interactions.ts +++ b/packages/ai/src/protocols/google-interactions.ts @@ -23,6 +23,7 @@ import { GeminiGenerateContent } from "./utils/gemini-generate-content.js" import { ToolStream } from "./utils/tool-stream.js" const ADAPTER = "google-interactions" +const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator" export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" // ============================================================================= @@ -259,6 +260,7 @@ const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest) { steps[steps.length - 1] = { type, content: [...last.content, content] } else steps.push({ type, content: [content] }) } + let hasSignedStep = false for (const part of message.content) { if (message.role === "tool") { if (part.type !== "tool-result") @@ -281,21 +283,25 @@ const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest) { continue } if (message.role === "assistant" && part.type === "reasoning") { + const thoughtSignature = signature(part.providerMetadata, key) steps.push({ type: "thought", - signature: signature(part.providerMetadata, key), + signature: thoughtSignature, summary: part.text ? [{ type: "text", text: part.text }] : undefined, }) + if (thoughtSignature !== undefined) hasSignedStep = true continue } if (message.role === "assistant" && part.type === "tool-call") { + const toolSignature = signature(part.providerMetadata, key) steps.push({ type: "function_call", id: part.id, name: part.name, arguments: part.input, - signature: signature(part.providerMetadata, key), + signature: toolSignature ?? (!hasSignedStep ? SKIP_THOUGHT_SIGNATURE_VALIDATOR : undefined), }) + if (toolSignature !== undefined) hasSignedStep = true continue } return yield* ProviderShared.unsupportedContent( diff --git a/packages/ai/src/protocols/mistral-chat.ts b/packages/ai/src/protocols/mistral-chat.ts index 284aad73a181..362fa64bb41b 100644 --- a/packages/ai/src/protocols/mistral-chat.ts +++ b/packages/ai/src/protocols/mistral-chat.ts @@ -254,7 +254,6 @@ const lowerToolCall = (part: ToolCallPart, normalizeID: (id: string) => string): const lowerAssistant = Effect.fnUntraced(function* ( message: LLMRequest["messages"][number], normalizeID: (id: string) => string, - prefix: boolean, ) { const structured = message.content.some( (part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.mistral?.thinking), @@ -271,8 +270,6 @@ const lowerAssistant = Effect.fnUntraced(function* ( if (part.type === "reasoning") { const native = part.providerMetadata?.mistral?.thinking if (structured && isMistralThinkingContent(native)) content.push(native) - else if (structured) content.push({ type: "text", text: part.text }) - else text.push(part.text) continue } if (part.type === "tool-call") { @@ -285,7 +282,6 @@ const lowerAssistant = Effect.fnUntraced(function* ( role: "assistant" as const, content: structured ? content : text.join(""), ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}), - ...(prefix ? { prefix: true as const } : {}), } }) @@ -361,15 +357,17 @@ const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest) { const hasNativeThinking = message.content.some( (part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.mistral?.thinking), ) - const text = message.content - .flatMap((part) => (part.type === "text" || part.type === "reasoning" ? [part.text] : [])) - .join("") + const text = message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("") if (!hasToolCalls && !hasNativeThinking && text.trim() === "") continue - messages.push(yield* lowerAssistant(message, normalizeID, !hasToolCalls && message === request.messages.at(-1))) + messages.push(yield* lowerAssistant(message, normalizeID)) continue } messages.push(...(yield* lowerToolResults(message, normalizeID))) } + const trailing = messages.at(-1) + if (trailing?.role === "assistant" && !trailing.tool_calls) { + messages[messages.length - 1] = { ...trailing, prefix: true } + } return messages }) diff --git a/packages/ai/src/provider-error.ts b/packages/ai/src/provider-error.ts index e9c697c8e726..c85dc115b5f2 100644 --- a/packages/ai/src/provider-error.ts +++ b/packages/ai/src/provider-error.ts @@ -42,6 +42,8 @@ const patterns = [ // Hugging Face Text Generation Inference, e.g. Together /`inputs` tokens \+ `max_new_tokens` must be <= \d+/i, /context window exceeds limit/i, + // LiteLLM's ContextWindowExceededError + /context window exceeded/i, /exceeded model token limit/i, /context[_ ]length[_ ]exceeded/i, /context length is only \d+ tokens/i, diff --git a/packages/ai/src/providers/meta.ts b/packages/ai/src/providers/meta.ts index eecbb8125479..13fac64f3b4f 100644 --- a/packages/ai/src/providers/meta.ts +++ b/packages/ai/src/providers/meta.ts @@ -138,7 +138,6 @@ export const configure = (input: Config = {}) => { const messages = (modelID: string | ModelID) => configuredMessages.model({ id: modelID, - compatibility: { requireSignature: false }, }) const media = MediaRoute.deployment(input, options.auth) const image = (modelID: string | ModelID) => MetaImages.model({ ...media, id: modelID }) diff --git a/packages/ai/src/providers/minimax.ts b/packages/ai/src/providers/minimax.ts index 18c69016199c..ec5d77e7333b 100644 --- a/packages/ai/src/providers/minimax.ts +++ b/packages/ai/src/providers/minimax.ts @@ -117,7 +117,10 @@ export const configure = (input: Config = {}) => { auth, } const messages = (modelID: string | ModelID) => - messagesRoute.with(defaults).model({ id: modelID }) + messagesRoute.with(defaults).model({ + id: modelID, + compatibility: { requireSignature: false }, + }) const chat = (modelID: string | ModelID) => chatRoute.with(defaults).model({ id: modelID, diff --git a/packages/ai/test/provider-error.test.ts b/packages/ai/test/provider-error.test.ts index 228f6afc533d..89b959e518ef 100644 --- a/packages/ai/test/provider-error.test.ts +++ b/packages/ai/test/provider-error.test.ts @@ -21,6 +21,8 @@ describe("provider error classification", () => { "Prompt exceeds max length", "The input is too long for the selected model.", "This text request is too large for the model's configured input limit.", + // LiteLLM's own check, before it calls the model. + "litellm.ContextWindowExceededError: litellm.BadRequestError: litellm._pre_call_checks: Context Window exceeded for given call. No models have context window large enough for this call.", ] expect(messages.every(isContextOverflow)).toBe(true) diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index 3dd67aa7b841..111eb09f5cf7 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -1246,13 +1246,11 @@ describe("Anthropic Messages route", () => { }), ) - it.effect("demotes unsigned reasoning when signatures are required", () => + it.effect("drops unsigned reasoning when signatures are required", () => Effect.gen(function* () { const prepared = yield* compileUnsignedReasoning(model) - expect(prepared.body.messages).toEqual([ - { role: "assistant", content: [{ type: "text", text: "unsigned reasoning" }] }, - ]) + expect(prepared.body.messages).toEqual([]) }), ) @@ -1272,19 +1270,19 @@ describe("Anthropic Messages route", () => { .model({ id: "kimi-k2.6" }) const codingPrepared = yield* compileUnsignedReasoning(coding.model({ id: "k3" })) const moonshotPrepared = yield* compileUnsignedReasoning(moonshot) + const qwenPrepared = yield* compileUnsignedReasoning(coding.with({ provider: "opencode" }).model({ id: "qwen3.8-flash" })) + const minimaxPrepared = yield* compileUnsignedReasoning( + coding.with({ provider: "opencode-go" }).model({ id: "minimax-m3" }), + ) - expect(codingPrepared.body.messages).toEqual([ - { - role: "assistant", - content: [{ type: "thinking", thinking: "unsigned reasoning", signature: "" }], - }, - ]) - expect(moonshotPrepared.body.messages).toEqual([ - { - role: "assistant", - content: [{ type: "thinking", thinking: "unsigned reasoning", signature: "" }], - }, - ]) + for (const prepared of [codingPrepared, moonshotPrepared, qwenPrepared, minimaxPrepared]) { + expect(prepared.body.messages).toEqual([ + { + role: "assistant", + content: [{ type: "thinking", thinking: "unsigned reasoning", signature: "" }], + }, + ]) + } }), ) @@ -1299,9 +1297,7 @@ describe("Anthropic Messages route", () => { .model({ id: "k3", compatibility: { requireSignature: true } }) const prepared = yield* compileUnsignedReasoning(compatible) - expect(prepared.body.messages).toEqual([ - { role: "assistant", content: [{ type: "text", text: "unsigned reasoning" }] }, - ]) + expect(prepared.body.messages).toEqual([]) }), ) diff --git a/packages/ai/test/provider/bedrock-converse-cache.test.ts b/packages/ai/test/provider/bedrock-converse-cache.test.ts index 3ec178fdcec5..263ebd3b7772 100644 --- a/packages/ai/test/provider/bedrock-converse-cache.test.ts +++ b/packages/ai/test/provider/bedrock-converse-cache.test.ts @@ -61,7 +61,7 @@ describe("Bedrock Converse cache policy", () => { { role: "user", content: [{ text: "Question" }, { text: "\nUpdate\n" }] }, { role: "assistant", - content: [{ text: "Answer" }, { text: "Unsigned reasoning" }, { toolUse: { name: "lookup" } }], + content: [{ text: "Answer" }, { toolUse: { name: "lookup" } }], }, { role: "user", content: [{ toolResult: { content: [{ json: "Result" }] } }] }, ], diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index c895ba159bd0..4a0b6f72f63b 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -1416,7 +1416,7 @@ describe("Bedrock Converse route", () => { for (const signature of [undefined, "", " "]) { for (const cache of ["none", "auto"] as const) { - it.effect(`demotes unsigned reasoning to text (${JSON.stringify(signature)}, cache: ${cache})`, () => + it.effect(`drops unsigned reasoning (${JSON.stringify(signature)}, cache: ${cache})`, () => Effect.gen(function* () { const prepared = yield* compileRequest( ai.request({ @@ -1436,10 +1436,15 @@ describe("Bedrock Converse route", () => { cache, }), ) - expect(prepared.body.messages[1]).toEqual({ - role: "assistant", - content: [{ text: "Partial thought" }, { cachePoint: { type: "default" } }], - }) + expect(prepared.body.messages).toEqual([ + { + role: "user", + content: + cache === "auto" + ? [{ text: "Think" }, { text: "Continue" }, { cachePoint: { type: "default" } }] + : [{ text: "Think" }, { text: "Continue" }], + }, + ]) }), ) } @@ -1471,7 +1476,7 @@ describe("Bedrock Converse route", () => { }), ) - it.effect("demotes foreign reasoning while preserving signed, redacted, text, and tool blocks", () => + it.effect("drops foreign reasoning while preserving signed, redacted, text, and tool blocks", () => Effect.gen(function* () { const cache = new CacheHint({ type: "ephemeral" }) const prepared = yield* compileRequest( @@ -1500,7 +1505,6 @@ describe("Bedrock Converse route", () => { { role: "assistant", content: [ - { text: "Foreign thought" }, { reasoningContent: { reasoningText: { text: "Signed thought", signature: "sig_1" } } }, { reasoningContent: { reasoningText: { text: "", signature: "sig_2" } } }, { reasoningContent: { redactedContent: "cmVkYWN0ZWQ=" } }, diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index a408907e80e4..967d28fa8790 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -175,7 +175,12 @@ describe("Gemini route", () => { expect(prepared.body.contents).toEqual([ { role: "model", - parts: [{ functionCall: { name: "lookup", args: { query: "weather" } } }], + parts: [ + { + functionCall: { name: "lookup", args: { query: "weather" } }, + thoughtSignature: "skip_thought_signature_validator", + }, + ], }, { role: "user", @@ -219,8 +224,14 @@ describe("Gemini route", () => { { role: "model", parts: [ - { functionCall: { name: "lookup", args: { query: "weather" } } }, - { functionCall: { name: "lookup", args: { query: "time" } } }, + { + functionCall: { name: "lookup", args: { query: "weather" } }, + thoughtSignature: "skip_thought_signature_validator", + }, + { + functionCall: { name: "lookup", args: { query: "time" } }, + thoughtSignature: "skip_thought_signature_validator", + }, ], }, { @@ -302,7 +313,15 @@ describe("Gemini route", () => { ) expect(legacy.body.contents).toEqual([ - { role: "model", parts: [{ functionCall: { name: "lookup", args: { query: "weather" } } }] }, + { + role: "model", + parts: [ + { + functionCall: { name: "lookup", args: { query: "weather" } }, + thoughtSignature: "skip_thought_signature_validator", + }, + ], + }, { role: "user", parts: [{ functionResponse: { name: "lookup", response: { name: "lookup", content: "done" } } }], @@ -331,7 +350,15 @@ describe("Gemini route", () => { ) expect(prepared.body.contents).toEqual([ - { role: "model", parts: [{ functionCall: { id: "call_1", name: "lookup", args: { query: "weather" } } }] }, + { + role: "model", + parts: [ + { + functionCall: { id: "call_1", name: "lookup", args: { query: "weather" } }, + thoughtSignature: "skip_thought_signature_validator", + }, + ], + }, { role: "user", parts: [ @@ -380,7 +407,12 @@ describe("Gemini route", () => { }, { role: "model", - parts: [{ functionCall: { name: "lookup", args: { query: "weather" } } }], + parts: [ + { + functionCall: { name: "lookup", args: { query: "weather" } }, + thoughtSignature: "skip_thought_signature_validator", + }, + ], }, { role: "user", @@ -434,7 +466,15 @@ describe("Gemini route", () => { ) expect(prepared.body.contents).toEqual([ - { role: "model", parts: [{ functionCall: { name: "read", args: { path: "pixel.png" } } }] }, + { + role: "model", + parts: [ + { + functionCall: { name: "read", args: { path: "pixel.png" } }, + thoughtSignature: "skip_thought_signature_validator", + }, + ], + }, { role: "user", parts: [ @@ -670,7 +710,13 @@ describe("Gemini route", () => { model: "gemini-3-flash-preview", input: [ { type: "user_input", content: [{ type: "video", uri, mime_type: "video/mp4" }] }, - { type: "function_call", id: "call_video", name: "read", arguments: {} }, + { + type: "function_call", + id: "call_video", + name: "read", + arguments: {}, + signature: "skip_thought_signature_validator", + }, { type: "function_result", call_id: "call_video", @@ -714,7 +760,10 @@ describe("Gemini route", () => { ) expect(prepared.body.contents).toEqual([ - { role: "model", parts: [{ functionCall: { name: "shot", args: {} } }] }, + { + role: "model", + parts: [{ functionCall: { name: "shot", args: {} }, thoughtSignature: "skip_thought_signature_validator" }], + }, { role: "user", parts: [{ functionResponse: { name: "shot", response: { name: "shot", content: "" } } }], @@ -764,7 +813,10 @@ describe("Gemini route", () => { expect(prepared.body.contents).toEqual([ { role: "model", - parts: [{ functionCall: { name: "shot", args: {} } }, { functionCall: { name: "shot", args: {} } }], + parts: [ + { functionCall: { name: "shot", args: {} }, thoughtSignature: "skip_thought_signature_validator" }, + { functionCall: { name: "shot", args: {} }, thoughtSignature: "skip_thought_signature_validator" }, + ], }, { role: "user", diff --git a/packages/ai/test/provider/google-vertex-interactions.test.ts b/packages/ai/test/provider/google-vertex-interactions.test.ts index e3b34d021696..172e0ddd1dcc 100644 --- a/packages/ai/test/provider/google-vertex-interactions.test.ts +++ b/packages/ai/test/provider/google-vertex-interactions.test.ts @@ -259,6 +259,49 @@ describe("Google Vertex Interactions", () => { ], }) expect(prepared.body.previous_interaction_id).toBeUndefined() + + const unsigned = yield* compileRequest( + ai.request({ + model, + messages: [ + Message.assistant([ + ToolCallPart.make({ id: "call_foreign", name: "lookup", input: { city: "Paris" } }), + ]), + Message.tool({ id: "call_foreign", name: "lookup", result: { weather: "sunny" } }), + Message.assistant([ + ToolCallPart.make({ + id: "call_signed", + name: "lookup", + input: { city: "Lyon" }, + providerMetadata: { vertex: { interactionSignature: "sig_1" } }, + }), + ToolCallPart.make({ id: "call_sibling", name: "lookup", input: { city: "Nice" } }), + ]), + Message.tool({ id: "call_signed", name: "lookup", result: { weather: "cloudy" } }), + Message.tool({ id: "call_sibling", name: "lookup", result: { weather: "warm" } }), + ], + }), + ) + expect(unsigned.body.input).toEqual([ + { + type: "function_call", + id: "call_foreign", + name: "lookup", + arguments: { city: "Paris" }, + signature: "skip_thought_signature_validator", + }, + { type: "function_result", call_id: "call_foreign", name: "lookup", result: { weather: "sunny" } }, + { + type: "function_call", + id: "call_signed", + name: "lookup", + arguments: { city: "Lyon" }, + signature: "sig_1", + }, + { type: "function_call", id: "call_sibling", name: "lookup", arguments: { city: "Nice" } }, + { type: "function_result", call_id: "call_signed", name: "lookup", result: { weather: "cloudy" } }, + { type: "function_result", call_id: "call_sibling", name: "lookup", result: { weather: "warm" } }, + ]) }), ) }) diff --git a/packages/ai/test/provider/mistral-chat.test.ts b/packages/ai/test/provider/mistral-chat.test.ts index 3f4cd8e3d7de..40411f0837f6 100644 --- a/packages/ai/test/provider/mistral-chat.test.ts +++ b/packages/ai/test/provider/mistral-chat.test.ts @@ -123,7 +123,7 @@ describe("Mistral Chat", () => { }, { role: "assistant", - content: "ThinkCalling", + content: "Calling", }, ]) const assistant = prepared.body.messages[3] @@ -177,6 +177,21 @@ describe("Mistral Chat", () => { { role: "assistant", content: "Prefix", prefix: true }, ]) + const trailingDropped = yield* compileRequest( + ai.request({ + model, + messages: [ + Message.user("Continue"), + Message.assistant("Prefix"), + Message.assistant({ type: "reasoning", text: "Dropped unsigned reasoning" }), + ], + }), + ) + expect(trailingDropped.body.messages).toEqual([ + { role: "user", content: "Continue" }, + { role: "assistant", content: "Prefix", prefix: true }, + ]) + const uncached = yield* compileRequest( ai.request({ model, @@ -227,6 +242,7 @@ describe("Mistral Chat", () => { messages: [ Message.assistant(" \n "), Message.assistant({ type: "reasoning", text: "\t" }), + Message.assistant({ type: "reasoning", text: "Foreign reasoning without mistral metadata" }), Message.assistant({ type: "tool-call", id: "Ab12Cd34E", name: "lookup", input: {} }), ], }), diff --git a/packages/app/component-tests/input-surfaces.spec.ts b/packages/app/component-tests/input-surfaces.spec.ts new file mode 100644 index 000000000000..b9679e188da6 --- /dev/null +++ b/packages/app/component-tests/input-surfaces.spec.ts @@ -0,0 +1,110 @@ +import { expect, story } from "../../stories/playwright/story" + +for (const control of ["text-input", "textarea"]) { + story(`${control} supports its surfaces with inset feedback and stable geometry`, async ({ mount, page }) => { + const root = await mount(`ui-${control}--surfaces`) + + const fields = root.locator(`[data-component="${control}-v2"]`) + + await page.addStyleTag({ + content: `[data-component="${control}-v2"] { + --v2-background-bg-base: rgb(20, 30, 40); + --v2-background-bg-layer-01: rgb(50, 60, 70); + --v2-overlay-simple-overlay-hover: rgba(10, 20, 30, 0.1); + --v2-elevation-input: 0px 0px 0px 3px rgb(1, 2, 3); + --v2-elevation-button-neutral: 0px 0px 0px 7px rgb(4, 5, 6); + }`, + }) + + await expect(fields.nth(0)).toHaveCSS("background-color", "rgb(20, 30, 40)") + await expect(fields.nth(1)).toHaveCSS("background-color", "rgb(50, 60, 70)") + await expect(fields.nth(0)).toHaveCSS("box-shadow", "rgb(1, 2, 3) 0px 0px 0px 3px") + await expect(fields.nth(1)).toHaveCSS("box-shadow", "rgb(1, 2, 3) 0px 0px 0px 3px") + + for (const field of await fields.all()) { + await expect(field).toHaveCSS("background-image", "none") + } + + const base = fields.nth(0) + + const geometry = await base.getByRole("textbox").boundingBox() + + await base.hover() + await expect + .poll(() => base.evaluate((element) => getComputedStyle(element).boxShadow)) + .toContain("rgb(1, 2, 3) 0px 0px 0px 3px") + await base.getByRole("textbox").fill("Still editable") + await expect(base.getByRole("textbox")).toHaveValue("Still editable") + await expect(base).toHaveCSS("outline-width", "1px") + await expect(base).toHaveCSS("outline-offset", "-1px") + await expect(base).not.toHaveCSS("outline-color", "rgba(0, 0, 0, 0)") + expect(await base.getByRole("textbox").boundingBox()).toEqual(geometry) + + const invalid = root.locator("[data-invalid]") + + const disabled = root.locator("[data-disabled]") + + await expect(invalid).toHaveCSS("outline-width", "1px") + await expect(invalid).toHaveCSS("outline-offset", "-1px") + await expect(invalid).not.toHaveCSS("outline-color", "rgba(0, 0, 0, 0)") + await expect(invalid.getByRole("textbox")).toHaveAttribute("aria-invalid", "true") + await expect(disabled.getByRole("textbox")).toBeDisabled() + + if (control === "textarea") return + + const invisible = root.locator('[data-surface="invisible"]').first() + + await base.getByRole("textbox").blur() + await expect(invisible).toHaveCSS("background-color", "rgba(0, 0, 0, 0)") + await invisible.hover() + await expect(invisible).toHaveCSS("background-color", "rgba(10, 20, 30, 0.1)") + await expect(invisible).toHaveCSS("box-shadow", "none") + await invisible.getByRole("textbox").focus() + await expect(invisible).toHaveCSS("background-color", "rgba(0, 0, 0, 0)") + await expect(invisible).toHaveCSS("outline-offset", "-1px") + await expect(invisible).not.toHaveCSS("outline-color", "rgba(0, 0, 0, 0)") + await expect(invalid).toHaveCSS("background-color", "rgba(0, 0, 0, 0)") + await expect(disabled).toHaveCSS("background-color", "rgba(0, 0, 0, 0)") + await expect(disabled).toHaveCSS("box-shadow", "none") + }) +} + +story( + "text inputs keep clear and copy actions equally sized and inset across sizes and directions", + async ({ mount, page }) => { + const root = await mount("ui-text-input--appearances") + + const fields = root.locator('[data-component="text-input-v2"]') + + for (const [index, height] of [24, 28, 32, 32, 24, 28, 32].entries()) { + await expect(fields.nth(index)).toHaveCSS("height", `${height}px`) + } + + for (const direction of ["ltr", "rtl"]) { + await page.evaluate((dir) => (document.documentElement.dir = dir), direction) + + for (const field of await fields.filter({ has: page.getByRole("button") }).all()) { + const button = field.getByRole("button") + + await expect(button).toHaveCSS("width", "20px") + await expect(button).toHaveCSS("height", "20px") + await expect(button).toHaveCSS("padding", "2px") + await expect(button).toHaveCSS("border-radius", "4px") + + expect( + await field.evaluate((element) => { + const bounds = element.getBoundingClientRect() + + const action = element.querySelector("button")!.getBoundingClientRect() + + return { + inset: + getComputedStyle(element).direction === "rtl" ? action.left - bounds.left : bounds.right - action.right, + center: action.y + action.height / 2 - bounds.y - bounds.height / 2, + } + }), + ).toEqual({ inset: 8, center: 0 }) + } + } + }, +) diff --git a/packages/app/e2e/regression/composer.spec.ts b/packages/app/e2e/regression/composer.spec.ts index e57638b7cb12..8ea8c93d44dd 100644 --- a/packages/app/e2e/regression/composer.spec.ts +++ b/packages/app/e2e/regression/composer.spec.ts @@ -1133,6 +1133,39 @@ test("/reload reloads the server's configuration once and reports it", async ({ const followUp = "Add follow-up, / for commands, @ for context…" +test("the shell shortcut toggles modes and keeps explicit bindings", async ({ page }) => { + const view = await openDraft(page, { + name: "ShellToggle", + seed: { + settings: { + keybinds: { "prompt.mode.shell": "mod+alt+s", "prompt.mode.normal": "mod+alt+e" }, + }, + }, + }) + + const modifier = await page.evaluate(() => (/Mac/i.test(navigator.platform) ? "Meta" : "Control")) + + const exit = page.getByRole("button", { name: /^Exit/ }) + + await view.editor.fill("echo hello") + await view.editor.press(`${modifier}+Shift+x`) + await expect(exit).toBeVisible() + await expect(view.editor).toHaveText("echo hello") + await view.editor.press(`${modifier}+Shift+x`) + await expect(exit).toHaveCount(0) + await expect(view.editor).toHaveText("echo hello") + await view.editor.press(`${modifier}+Shift+x`) + await expect(exit).toBeVisible() + await view.editor.press("Escape") + await expect(exit).toHaveCount(0) + await expect(view.editor).toBeFocused() + await view.editor.press(`${modifier}+Alt+s`) + await expect(exit).toBeVisible() + await view.editor.press(`${modifier}+Alt+e`) + await expect(exit).toHaveCount(0) + await expect(view.editor).toHaveText("echo hello") +}) + for (const row of [ { state: "an idle", copy: "Ask anything, / for commands, @ for context…" }, { state: "a running", copy: followUp, sessionStatus: { ses_placeholder: { type: "running" } } }, diff --git a/packages/app/e2e/regression/new-session.spec.ts b/packages/app/e2e/regression/new-session.spec.ts index d2413619ba08..4f1ee96867d5 100644 --- a/packages/app/e2e/regression/new-session.spec.ts +++ b/packages/app/e2e/regression/new-session.spec.ts @@ -58,7 +58,7 @@ test("the new session screen does not show session details", async ({ page }) => await expect(page.getByRole("button", { name: "Session details", exact: true })).toHaveCount(0) }) -test("the location shortcut keeps cycling after hovering its icon", async ({ page }) => { +test("the location shortcuts choose worktrees from every state and keep cycling", async ({ page }) => { const existing = "C:/OpenCode/LocationShortcut/.worktrees/quiet-river" const view = await openDraft(page, { @@ -67,27 +67,133 @@ test("the location shortcut keeps cycling after hovering its icon", async ({ pag }) const modifier = await page.evaluate(() => (/Mac/i.test(navigator.platform) ? "Meta" : "Control")) + const path = page.locator('[data-slot="new-session-location-path"]') - const icon = page.locator('[data-slot="new-session-location-icon"]') - // The worktree trigger already says Local, so the local path has no icon of its own. + const search = page.getByRole("combobox", { name: "Search worktrees", exact: true }) + await expect(path).toHaveText(view.directory) - await expect(icon).toHaveCount(0) - await newSessionWorktree(page).click() + await view.editor.press(`${modifier}+Alt+KeyO`) + await expect(search).toBeFocused() await page.getByRole("option", { name: /^quiet-river / }).click() await expect(view.editor).toBeFocused() await expect(path).toHaveText(existing) - await icon.hover() - await expect(page.getByRole("tooltip")).toContainText("Toggle session location") - await view.editor.press(`${modifier}+Alt+KeyL`) - await expect(newSessionWorktree(page)).toHaveText("Local") - await expect(icon).toHaveCount(0) - await expect(view.editor).toBeFocused() + await expect(page.locator('[data-slot="new-session-location-icon"]')).toHaveCount(0) + await view.editor.press(`${modifier}+Alt+KeyO`) + await expect(search).toBeFocused() + await page.getByRole("option", { name: /LocationShortcut C:\/OpenCode\/LocationShortcut$/ }).click() await view.editor.press(`${modifier}+Alt+KeyL`) await expect(newSessionWorktree(page)).toContainText("New worktree") + await view.editor.press(`${modifier}+Alt+KeyO`) + await expect(search).toBeFocused() + await page.getByRole("option", { name: /^quiet-river / }).click() + await expect(path).toHaveText(existing) await expect(view.editor).toBeFocused() }) +for (const binding of ["custom", "none"]) { + test(`the picker tooltips reflect ${binding} shortcuts`, async ({ page }) => { + const view = await openDraft(page, { + name: "PickerShortcuts", + seed: { + settings: { + keybinds: { + "project.select": binding === "custom" ? "mod+alt+p" : "none", + "session.location.select": binding === "custom" ? "mod+alt+x" : "none", + "session.location.cycle": binding === "custom" ? "mod+alt+k" : "none", + }, + }, + }, + }) + + const tooltip = page.getByRole("tooltip").and(page.locator("[data-expanded]")) + + const keys = tooltip.locator('[data-component="keybind-v2"]') + + await newSessionLocation(page).hover() + await expect(tooltip).toContainText("Choose project") + + await expect(keys).toHaveText(binding === "custom" ? [/P$/] : []) + await newSessionWorktree(page).hover() + await expect(tooltip).toContainText("Isolate sessions with worktrees") + + await expect(keys).toHaveText(binding === "custom" ? [/X$/] : []) + + const modifier = await page.evaluate(() => (/Mac/i.test(navigator.platform) ? "Meta" : "Control")) + + if (binding === "none") { + await view.editor.press(`${modifier}+Alt+o`) + await expect(page.getByRole("dialog")).toHaveCount(0) + await expect(newSessionWorktree(page)).toHaveText("Local") + + return + } + + await view.editor.press(`${modifier}+Alt+k`) + await expect(newSessionWorktree(page)).toContainText("New worktree") + await view.editor.press(`${modifier}+Alt+x`) + const search = page.getByRole("combobox", { name: "Search worktrees", exact: true }) + + await expect(search).toBeFocused() + await search.press("Escape") + await expect(page.getByRole("dialog")).toHaveCount(0) + await expect(newSessionWorktree(page)).toContainText("New worktree") + await expect(view.editor).toBeFocused() + await view.editor.press(`${modifier}+Alt+p`) + await expect(page.getByRole("combobox", { name: "Search projects", exact: true })).toBeFocused() + }) +} + +test("the worktree picker shortcut is disabled for non-Git projects", async ({ page }) => { + const view = await openDraft(page, { name: "NoWorktrees", project: { vcs: undefined } }) + + const modifier = await page.evaluate(() => (/Mac/i.test(navigator.platform) ? "Meta" : "Control")) + + await expect(newSessionWorktree(page)).toHaveCount(0) + await view.editor.press(`${modifier}+Alt+o`) + await expect(page.getByRole("dialog")).toHaveCount(0) + await view.editor.fill("Keep working locally") + await expect(view.editor).toHaveText("Keep working locally") +}) + +test("picker scrollbars overlay full-width rows and support dragging", async ({ page }) => { + const root = "C:/OpenCode/OverlayMenus" + + await openDraft(page, { + name: "OverlayMenus", + worktrees: () => [ + { directory: root }, + ...Array.from({ length: 24 }, (_, index) => ({ + directory: `${root}/worktree-${index}`, + strategy: "git" as const, + })), + ], + }) + await newSessionWorktree(page).click() + await expect(page.getByRole("combobox", { name: "Search worktrees", exact: true })).toBeFocused() + + const menu = page.locator('[data-component="menu-scroll"]') + + const viewport = menu.locator('[data-slot="menu-scroll-viewport"]') + + const thumb = menu.locator('[data-slot="menu-scroll-thumb"]') + + await menu.hover() + await expect(thumb).toHaveCSS("opacity", "1") + await expect.poll(() => viewport.evaluate((el) => el.getBoundingClientRect().width - el.clientWidth)).toBe(0) + + const track = (await menu.boundingBox())! + + const handle = (await thumb.boundingBox())! + + await thumb.hover() + await page.mouse.down() + await page.mouse.move(handle.x + handle.width / 2, track.y + track.height - 4) + await page.mouse.up() + await expect.poll(() => viewport.evaluate((el) => el.scrollTop > 0)).toBe(true) + await expect(page.getByRole("option", { name: /^worktree-23 / })).toBeInViewport() +}) + for (const row of [ { name: "POSIX", @@ -108,6 +214,21 @@ for (const row of [ await expect.poll(() => path.evaluate((el) => el.scrollWidth <= el.clientWidth)).toBe(true) await path.hover() await expect(page.getByRole("tooltip")).toHaveText(row.directory) + + if (row.name !== "POSIX") return + await expect(path).toHaveCSS("user-select", "text") + await path.click() + await path.evaluate((element) => { + const range = document.createRange() + + range.selectNodeContents(element) + window.getSelection()?.removeAllRanges() + window.getSelection()?.addRange(range) + }) + const modifier = await page.evaluate(() => (/Mac/i.test(navigator.platform) ? "Meta" : "Control")) + + await page.keyboard.press(`${modifier}+c`) + await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(row.directory) }) } @@ -234,11 +355,7 @@ test("the worktree trigger opens the current project's worktree step", async ({ await local.click() await expect(search).toBeFocused() await expect(back).toHaveText("Run session in") - await expect(options.locator(".command-palette-title")).toHaveText([ - "BranchWorktrees", - "New worktree", - "quiet-river", - ]) + await expect(options.locator(".command-palette-title")).toHaveText(["BranchWorktrees", "New worktree", "quiet-river"]) await expect(options.nth(0)).toHaveAttribute("aria-selected", "true") // Stepping back lands on the projects step with the drilled project active, as after drilling in with →. @@ -247,15 +364,28 @@ test("the worktree trigger opens the current project's worktree step", async ({ await expect(projects).toBeFocused() await expect(back).toHaveCount(0) await expect(options.filter({ hasText: "BranchWorktrees" })).toHaveAttribute("aria-selected", "true") + await projects.press("ArrowRight") + await expect(search).toBeFocused() + await search.press("Escape") + await expect(projects).toBeFocused() await projects.press("Escape") await expect(dialog).toHaveCount(0) await expect(view.editor).toBeFocused() - // Escape also steps back first, then closes. + // A directly opened worktree picker closes on Escape without showing projects first. await local.focus() await page.keyboard.press("Enter") await expect(search).toBeFocused() await search.press("Escape") + await expect(dialog).toHaveCount(0) + await expect(view.editor).toBeFocused() + + // Drilling in from the project picker still makes Escape step back. + await newSessionLocation(page).click() + await expect(projects).toBeFocused() + await projects.press("ArrowRight") + await expect(search).toBeFocused() + await search.press("Escape") await expect(projects).toBeFocused() await projects.press("Escape") await expect(dialog).toHaveCount(0) @@ -277,7 +407,7 @@ test("the branch shortcut chooses a base branch and stays inactive on Local", as const modifier = await page.evaluate(() => (/Mac/i.test(navigator.platform) ? "Meta" : "Control")) - const shortcut = `${modifier}+Alt+Period` + const shortcut = `${modifier}+Alt+KeyB` const base = page.getByRole("button", { name: "from dev", exact: true }) @@ -285,7 +415,7 @@ test("the branch shortcut chooses a base branch and stays inactive on Local", as await base.hover() await expect(page.getByRole("tooltip")).toContainText("Choose base branch") - await expect(page.getByRole("tooltip").locator('[data-component="keybind-v2"]')).toContainText(".") + await expect(page.getByRole("tooltip").locator('[data-component="keybind-v2"]')).toContainText("B") await expect(view.editor).toBeFocused() await view.editor.press(shortcut) await expect(search).toBeFocused() diff --git a/packages/app/e2e/regression/settings.spec.ts b/packages/app/e2e/regression/settings.spec.ts index 86f63ee26341..458da4daa79e 100644 --- a/packages/app/e2e/regression/settings.spec.ts +++ b/packages/app/e2e/regression/settings.spec.ts @@ -885,7 +885,13 @@ test.describe("pages open before slow data", () => { { name: "stripe", integrationID: "stripe-oauth", status: { status: "needs_auth", error: "Sign in required" } }, ], integrations: [ - { id: "stripe-oauth", name: "Stripe", methods: [{ id: "oauth", type: "oauth" }], connections: [] }, + { + id: "stripe-oauth", + name: "Stripe", + methods: [{ id: "oauth", type: "oauth" }], + connections: [], + metadata: { source: "mcp" }, + }, ], onIntegrationOAuth: (input) => { signIns.push(input.integrationID) @@ -1369,6 +1375,13 @@ test("providers: the connect dialog lists connected providers and manages their connections: accounts.toSorted((a, b) => Number(b.id === state.active) - Number(a.id === state.active)), }, { id: "anthropic", name: "Anthropic", methods: [{ type: "key", label: "API key" }], connections: [] }, + { + id: "linear", + name: "Linear", + methods: [{ id: "oauth", type: "oauth", label: "Sign in" }], + connections: [], + metadata: { source: "mcp" }, + }, ], onCredentialActivate: (credentialID) => { activated.push(credentialID) @@ -1389,6 +1402,9 @@ test("providers: the connect dialog lists connected providers and manages their await expect( dialog.locator('[data-provider-id="anthropic"]').locator('[data-slot="provider-connected"]'), ).toHaveCount(0) + // MCP servers connect from the MCPs dialog and settings, not here. + await expect(dialog.locator('[data-provider-id="linear"]')).toHaveCount(0) + await expect(dialog.getByText("MCP", { exact: true })).toHaveCount(0) const container = page.locator('[data-slot="dialog-container"]') await expect.poll(async () => (await container.boundingBox())?.height).toBe(512) const picker = await container.boundingBox() diff --git a/packages/app/e2e/regression/timeline-stream.spec.ts b/packages/app/e2e/regression/timeline-stream.spec.ts index ef0608d0c8a4..2d4ff7b5a652 100644 --- a/packages/app/e2e/regression/timeline-stream.spec.ts +++ b/packages/app/e2e/regression/timeline-stream.spec.ts @@ -3,6 +3,7 @@ import type { SessionMessageAssistant, SessionMessageInfo, ShellInfo } from "@op import { timelinePresets } from "@opencode/session-ui/timeline/detail" import { createTwoFilesPatch } from "diff" import { readSessionDraftText } from "../utils/drafts" +import { holdRoute } from "../utils/app" import { assistantID, assistantMessage, @@ -2005,6 +2006,7 @@ test("quotes plain timeline text into the composer, with or without a comment", const user = '[data-slot="user-message-text"]' const answer = `[data-timeline-part-id="${renderedPartID("prt_quote_answer")}"] [data-slot="text-part-body"]` await expect(page.locator(answer).getByText("bundle", { exact: true })).toBeVisible() + await expect(page.locator('[data-component="composer-editor"]')).toBeFocused() const select = (from: string, to: string) => page.evaluate( @@ -2050,7 +2052,7 @@ test("quotes plain timeline text into the composer, with or without a comment", await comment.fill("Why three?") await expect(popover.getByRole("button")).toHaveCount(1) await expect(popover.getByRole("button", { name: "Add comment", exact: true })).toBeVisible() - await popover.getByRole("button", { name: "Add comment", exact: true }).click() + await comment.press("Alt+Enter") await expect(comment).toHaveCount(0) await expect(chips.nth(1)).toContainText("Why three?") await chips.nth(1).click() @@ -2110,6 +2112,151 @@ test("quotes plain timeline text into the composer, with or without a comment", await expect(chips).toHaveCount(0) }) +test("quote popup sends independently without consuming the draft or other quotes", async ({ page }) => { + const sent: string[] = [] + + await setupTimeline(page, { + messages: [userMessage(), assistantMessage([textPart("prt_direct_quote", "Keep this quoted passage.")])], + onPrompt: (input) => void sent.push(String(input.body.delivery)), + }) + + const answer = page.locator( + `[data-timeline-part-id="${renderedPartID("prt_direct_quote")}"] [data-slot="text-part-body"]`, + ) + + const editor = page.locator('[data-component="composer-editor"]') + + const quote = page + .getByRole("toolbar", { name: "Quote selection" }) + .getByRole("button", { name: "Quote", exact: true }) + + await expect(answer).toHaveText("Keep this quoted passage.") + await editor.fill("Keep my separate draft") + await selectQuoteText(answer) + await quote.click({ modifiers: ["Alt"] }) + await expect(editor).toBeFocused() + await selectQuoteText(answer) + await quote.click() + const popup = page.locator('[data-component="composer-quote-comment"]') + + const comment = popup.locator("textarea") + + const action = popup.getByRole("button") + + const modifier = await page.evaluate(() => (/(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control")) + + await expect(comment).toBeFocused() + await comment.fill("Explain this please") + await action.focus() + await page.keyboard.down(modifier) + await expect(action).toHaveAccessibleName("Send quote") + await page.keyboard.press("Enter") + await page.keyboard.up(modifier) + await expect(popup).toHaveCount(0) + await expect.poll(() => sent).toEqual(["steer"]) + await expect(editor).toHaveText("Keep my separate draft") + const remaining = page.locator('[data-component="composer-attachments"] [data-component="attachment-card"]') + + await expect(remaining).toHaveCount(1) + await expect(remaining).toContainText("Keep this quoted passage.") + await expect(remaining).not.toContainText("Explain this please") +}) + +test("a pending quote send hides mentions and keeps the comment read-only", async ({ page }) => { + const sent: string[] = [] + + await setupTimeline(page, { + settings: { followUpBehavior: "steer" }, + sessionStatus: { [sessionID]: { type: "busy" } }, + messages: [userMessage(), assistantMessage([textPart("prt_quote_mentions", "Keep this quoted passage.")])], + findFiles: () => ["src/api.ts"], + onPrompt: (input) => void sent.push(String(input.body.delivery)), + }) + + const answer = page.locator( + `[data-timeline-part-id="${renderedPartID("prt_quote_mentions")}"] [data-slot="text-part-body"]`, + ) + + await expect(answer).toHaveText("Keep this quoted passage.") + await expect(page.locator('[data-component="composer-editor"]')).toBeFocused() + await selectQuoteText(answer) + await page + .getByRole("toolbar", { name: "Quote selection" }) + .getByRole("button", { name: "Quote", exact: true }) + .click() + const popup = page.locator('[data-component="composer-quote-comment"]') + + const comment = popup.locator("textarea") + + const modifier = await page.evaluate(() => (/(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control")) + + await comment.fill("Explain @src/a") + await expect(popup.locator('[data-slot="line-comment-v2-mention-item"]')).toHaveCount(1) + const admission = await holdRoute(page, new RegExp(`/api/session/${sessionID}/prompt(\\?|$)`), { method: "POST" }) + + await popup.locator('[data-slot="line-comment-v2-submit"]').click({ modifiers: [modifier] }) + await admission.arrived + await expect(popup.getByRole("button")).toBeDisabled() + await expect(comment).toHaveAttribute("readonly", "") + await expect(popup.locator('[data-component="loader-v2"]')).toBeVisible() + await expect(popup.locator('[data-slot="line-comment-v2-mention-item"]')).toHaveCount(0) + await comment.press("Tab") + await expect(comment).toHaveValue("Explain @src/a") + expect(admission.requests).toHaveLength(1) + admission.release() + await expect(popup).toHaveCount(0) + await expect.poll(() => sent).toEqual(["queue"]) +}) + +function selectQuoteText(target: Locator) { + return target.evaluate((element) => { + const range = document.createRange() + + range.selectNodeContents(element) + window.getSelection()?.removeAllRanges() + window.getSelection()?.addRange(range) + }) +} + +test("a failed direct quote send keeps the popup and comment for retry", async ({ page }) => { + let reject = true + + await setupTimeline(page, { + messages: [userMessage(), assistantMessage([textPart("prt_direct_quote_failure", "Keep this quoted passage.")])], + onPrompt: () => { + if (reject) throw new Error("Quote send failed") + }, + }) + + const answer = page.locator( + `[data-timeline-part-id="${renderedPartID("prt_direct_quote_failure")}"] [data-slot="text-part-body"]`, + ) + + await expect(answer).toHaveText("Keep this quoted passage.") + await selectQuoteText(answer) + await page + .getByRole("toolbar", { name: "Quote selection" }) + .getByRole("button", { name: "Quote", exact: true }) + .click() + const popup = page.locator('[data-component="composer-quote-comment"]') + + const comment = popup.locator("textarea") + + const modifier = await page.evaluate(() => (/(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control")) + + await comment.fill("Keep my latest comment") + await comment.press(`${modifier}+Enter`) + await expect(popup.getByRole("alert")).toHaveText("Couldn’t send. Your comment is still here. Try again.") + await expect(comment).toHaveValue("Keep my latest comment") + await expect(comment).toBeEditable() + reject = false + await popup.getByRole("button").click({ modifiers: [modifier] }) + await expect(popup).toHaveCount(0) + await expect(page.locator('[data-component="composer-attachments"] [data-component="attachment-card"]')).toHaveCount( + 0, + ) +}) + test("highlights the exact duplicate passage whenever its quote comment is open", async ({ page }) => { let reject = true await setupTimeline(page, { @@ -2230,6 +2377,7 @@ test("highlights the exact duplicate passage whenever its quote comment is open" await comment.press("Enter") await expect(comment).toHaveCount(0) await expect.poll(highlights).toEqual([[]]) + await expect(editor).toBeFocused() await select(second, 23) await toolbar.getByRole("button", { name: "Quote", exact: true }).click({ modifiers: ["Alt"] }) diff --git a/packages/app/package.json b/packages/app/package.json index cd67034c4541..7fc971833bf8 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode/app", - "version": "2.2.4", + "version": "2.2.5", "description": "", "type": "module", "exports": { diff --git a/packages/app/src/composer/editor/editor.tsx b/packages/app/src/composer/editor/editor.tsx index ed030bfb1d89..47061d0ef6bd 100644 --- a/packages/app/src/composer/editor/editor.tsx +++ b/packages/app/src/composer/editor/editor.tsx @@ -270,6 +270,7 @@ export function ComposerEditor(props: ComposerEditorProps) { onQuoteFocus={props.controller.focusQuotePrompt} onEditQuote={props.controller.editQuote} onQuoteComment={(quote, comment) => props.controller.commentQuote(quote.key, comment)} + quoteSubmit={view.submit.quote} searchPaths={props.controller.searchPaths} /> @@ -686,6 +687,7 @@ function ComposerAttachments(props: { onQuoteFocus?: () => void onEditQuote?: (key?: string) => void onQuoteComment?: (quote: Extract, comment: string) => void + quoteSubmit?: ComposerEditorModel["view"]["submit"]["quote"] searchPaths?: (query: string) => Promise }) { const i18n = useI18n() @@ -882,6 +884,13 @@ function ComposerAttachments(props: { text={current.text} comment={current.comment ?? ""} mention={(query) => props.searchPaths?.(query) ?? Promise.resolve([])} + send={ + props.quoteSubmit && { + available: props.quoteSubmit.available(), + delivery: props.quoteSubmit.delivery(), + onSubmit: (comment) => props.quoteSubmit!.onSubmit(current.key, comment), + } + } onSubmit={(comment) => { props.onQuoteComment?.(current, comment) props.onEditQuote?.() diff --git a/packages/app/src/composer/editor/interaction.ts b/packages/app/src/composer/editor/interaction.ts index 0044a37e4161..315ee2a7762c 100644 --- a/packages/app/src/composer/editor/interaction.ts +++ b/packages/app/src/composer/editor/interaction.ts @@ -23,7 +23,7 @@ import { type ComposerInteractionEvent, } from "../suggestions/machine" import { clonePrompt, isAttachment, promptLength } from "../prompt-parts" -import type { ComposerQueue } from "../adapter" +import type { ComposerDelivery, ComposerQueue } from "../adapter" import { isCommentItem } from "../state" import type { ComposerQuoteAnchor } from "@opencode/plugin/gui" @@ -49,6 +49,11 @@ export type ComposerEditorView = { held?: Accessor working?: Accessor queue?: ComposerQueue + quote?: { + available: Accessor + delivery: Accessor + onSubmit: (key: string, comment: string) => Promise + } onSubmit: (options?: { alternate?: boolean }) => void onStop: () => void } diff --git a/packages/app/src/composer/editor/quote-comment.tsx b/packages/app/src/composer/editor/quote-comment.tsx index f59349d48461..3ebaf952cce8 100644 --- a/packages/app/src/composer/editor/quote-comment.tsx +++ b/packages/app/src/composer/editor/quote-comment.tsx @@ -1,4 +1,4 @@ -import { createMemo, onCleanup } from "solid-js" +import { createMemo, onCleanup, Show } from "solid-js" import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" import { makeEventListener } from "@solid-primitives/event-listener" @@ -6,6 +6,15 @@ import { createResizeObserver } from "@solid-primitives/resize-observer" import { LineCommentEditor } from "@opencode/ui/line-comment" import { useLanguage } from "@/runtime/i18n/language" import type { ComposerQuoteAnchor } from "@opencode/plugin/gui" +import type { ComposerDelivery } from "../adapter" + +const IS_MAC = /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) + +const sendLabels = { + queue: "session.quote.queue", + steer: "session.quote.steer", + send: "session.quote.send", +} as const const GAP = 8 @@ -19,6 +28,11 @@ export function QuoteCommentEditor(props: { mention: (query: string) => Promise onSubmit: (comment: string) => void onClose: (focus?: boolean) => void + send?: { + available: boolean + delivery: ComposerDelivery | undefined + onSubmit: (comment: string) => Promise + } }) { const language = useLanguage() @@ -30,6 +44,8 @@ export function QuoteCommentEditor(props: { revision: 0, height: 0, openingHeight: 0, + sending: false, + failed: false, }) const width = () => Math.min(WIDTH, state.viewport - GAP * 2) @@ -47,6 +63,8 @@ export function QuoteCommentEditor(props: { let frame: number | undefined + let mounted = true + const schedule = () => { if (frame !== undefined) return frame = requestAnimationFrame(() => { @@ -61,6 +79,8 @@ export function QuoteCommentEditor(props: { } onCleanup(() => { + mounted = false + if (frame !== undefined) cancelAnimationFrame(frame) }) @@ -72,7 +92,7 @@ export function QuoteCommentEditor(props: { if (target instanceof Node && (element?.contains(target) || props.anchor.contains(target))) return - if (state.draft.trim() === props.comment.trim()) props.onClose() + if (!state.sending && state.draft.trim() === props.comment.trim()) props.onClose() }) const left = () => Math.min(Math.max(source()?.left ?? state.rect.left, GAP), state.viewport - width() - GAP) @@ -88,6 +108,28 @@ export function QuoteCommentEditor(props: { const availableHeight = () => Math.max(0, bottom() - GAP) const top = () => Math.max(GAP, bottom() - state.height) + const alternateSubmit = createMemo(() => { + const send = props.send + + if (!send) return + + return { + label: language.t(sendLabels[send.delivery ?? "send"]), + keys: [IS_MAC ? "⌘" : language.t("common.key.ctrl"), "⏎"], + disabled: !send.available, + onSubmit: async (comment: string) => { + setState({ sending: true, failed: false }) + + const sent = await send.onSubmit(comment) + + if (!mounted) return + setState({ sending: false, failed: !sent }) + + if (sent) props.onClose() + }, + } + }) + return (
setState("draft", value)} onCancel={() => props.onClose(true)} onSubmit={props.onSubmit} + submitting={state.sending} + alternateSubmit={alternateSubmit()} placeholder={language.t("session.quote.optionalCommentPlaceholder")} submitLabel={language.t( props.comment.trim() @@ -137,6 +181,14 @@ export function QuoteCommentEditor(props: { mention={{ items: props.mention }} selection={{props.text}} /> + + +
diff --git a/packages/app/src/composer/model.ts b/packages/app/src/composer/model.ts index 2350812be0b7..3ac0318debee 100644 --- a/packages/app/src/composer/model.ts +++ b/packages/app/src/composer/model.ts @@ -536,6 +536,15 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue? held: submission.held, working: adapter.working, queue: options?.queue, + quote: + adapter.kind === "active-session" + ? { + available: () => + available() && !adapter.controls().model.loading && !submission.held() && !options?.queue?.undoing(), + delivery: () => (adapter.working() ? (options?.queue?.alternate() ?? "steer") : undefined), + onSubmit: submission.submitQuote, + } + : undefined, onSubmit: (submitOptions) => { if (!available()) return const queue = options?.queue @@ -570,11 +579,18 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue? disabled: controller.state.mode !== "normal", onSelect: () => controller.attach(), }, + { + id: "prompt.mode.toggle", + title: language.t("command.prompt.mode.toggle"), + category: language.t("command.category.session"), + keybind: "mod+shift+x", + onSelect: () => + void controller.dispatch({ type: controller.state.mode === "normal" ? "mode.shell" : "mode.normal" }), + }, { id: "prompt.mode.shell", title: language.t("command.prompt.mode.shell"), category: language.t("command.category.session"), - keybind: "mod+shift+x", disabled: controller.state.mode === "shell", onSelect: () => void controller.dispatch({ type: "mode.shell" }), }, @@ -582,7 +598,6 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue? id: "prompt.mode.normal", title: language.t("command.prompt.mode.normal"), category: language.t("command.category.session"), - keybind: "mod+shift+e", disabled: controller.state.mode === "normal", onSelect: () => void controller.dispatch({ type: "mode.normal" }), }, diff --git a/packages/app/src/composer/state.ts b/packages/app/src/composer/state.ts index 13fdf1669da8..e3a38b70d4a4 100644 --- a/packages/app/src/composer/state.ts +++ b/packages/app/src/composer/state.ts @@ -185,17 +185,19 @@ function createComposerStateValue(store: ComposerStore, setStore: SetStoreFuncti }, /** Edits the file comment, note, or quote with this commentID; only file comments carry a preview. */ updateComment(commentID: string, next: { readonly comment?: string; readonly preview?: string }) { - setStore("context", "items", (items) => - items.map((item) => { - if (item.commentID !== commentID) return item - - if (item.type === "quote") return { ...item, comment: next.comment } + setStore( + "context", + "items", + (item) => item.commentID === commentID, + (item) => { + // Preserve quote identity so an open comment editor does not remount. + if (item.type === "quote") return { comment: next.comment } const value = item.type === "file" ? { ...item, ...next } : { ...item, comment: next.comment ?? item.comment } return { ...value, key: contextItemKey(value) } - }), + }, ) clearRetry() }, diff --git a/packages/app/src/composer/submit.test.ts b/packages/app/src/composer/submit.test.ts index 0d0caa00ae36..79c84013fd78 100644 --- a/packages/app/src/composer/submit.test.ts +++ b/packages/app/src/composer/submit.test.ts @@ -84,14 +84,14 @@ function submitInput( commands: () => readonly { name: string }[] | undefined = () => [], history: string[] = [], clientCommand?: (text: string) => (() => void | Promise) | undefined, - delivery: ComposerDelivery = "steer", + delivery: (alternate: boolean) => ComposerDelivery = () => "steer", ) { return createComposerSubmit({ adapter, mode: () => mode, commands, clientCommand, - delivery: () => delivery, + delivery, editor: () => undefined, queueScroll() {}, addToHistory: (prompt) => @@ -176,6 +176,108 @@ function session(input: { } describe("Composer submission", () => { + test.each(["steer", "queue"] as const)( + "sends only the selected quote as a %s, including its comment mentions, without clearing the draft", + async (delivery) => { + const state = createMemoryComposerState({ prompt: "/review keep this draft" }).capture() + + const image = { + type: "image" as const, + id: "unrelated-image", + filename: "draft.png", + mime: "image/png", + blob: { id: "unrelated-image", url: "data:image/png;base64,YQ==" }, + } + + state.set([...state.current(), image], 5) + state.mode.set("shell") + state.context.add({ type: "quote", text: "Selected passage", commentID: "selected", comment: "Old comment" }) + state.context.add({ type: "quote", text: "Unrelated passage", commentID: "other" }) + state.context.add({ type: "file", path: "unrelated.ts" }) + const before = state.current() + + const requests: Parameters[0][] = [] + + const submission = submitInput( + active(state, session({ calls: [], prompt: async (value) => void requests.push(value) })), + undefined, + "shell", + undefined, + undefined, + undefined, + (alternate) => { + expect(alternate).toBe(true) + + return delivery + }, + ) + + expect(await submission.submitQuote("quote:c=selected", "Compare @src/api.ts")).toBe(true) + expect(requests).toHaveLength(1) + expect(requests[0]?.text).toBe( + "The user quoted the following from earlier in the conversation:\n> Selected passage\n\nThe user made the following comment regarding this quote: Compare @src/api.ts", + ) + expect(requests[0]).toMatchObject({ + delivery, + files: [{ uri: "file:///C:/repo/src/api.ts", name: "api.ts" }], + agents: [], + skills: [], + metadata: { + displayText: "", + comments: [{ type: "quote", text: "Selected passage", comment: "Compare @src/api.ts" }], + attachments: [], + }, + }) + expect(state.current()).toBe(before) + expect(state.cursor()).toBe(5) + expect(state.mode.current()).toBe("shell") + expect(state.context.items().map((item) => item.key)).toEqual([ + "quote:c=other", + "file:unrelated.ts:undefined:undefined", + ]) + }, + ) + + test("a failed quote send retains its latest comment and prevents duplicate submissions while pending", async () => { + const state = createMemoryComposerState({ prompt: "Keep this draft" }).capture() + + state.context.add({ type: "quote", text: "Selected passage", commentID: "selected" }) + const failed = Promise.withResolvers() + + const started = Promise.withResolvers() + + const errors: string[] = [] + + const calls: string[] = [] + + const submission = submitInput( + active( + state, + session({ + calls, + prompt: async () => { + started.resolve() + await failed.promise + }, + }), + ), + { missingSelection() {}, unqueueable() {}, failed: (_kind, error) => void errors.push(String(error)) }, + ) + + const pending = submission.submitQuote("quote:c=selected", "Latest comment") + + await started.promise + expect(await submission.submitQuote("quote:c=selected", "Duplicate")).toBe(false) + failed.reject(new Error("Could not send")) + expect(await pending).toBe(false) + expect(errors).toHaveLength(1) + expect(state.current()).toEqual([{ type: "text", content: "Keep this draft", start: 0, end: 15 }]) + expect(state.context.items()).toMatchObject([ + { key: "quote:c=selected", text: "Selected passage", comment: "Latest comment" }, + ]) + expect(calls.filter((call) => call === "send")).toHaveLength(2) + }) + test("applies the selection and runs a client argument command without admitting it to the session", async () => { const state = createMemoryComposerState().capture() @@ -319,9 +421,15 @@ describe("Composer submission", () => { switchModel: async () => committed.promise, }) - await submitInput(active(state, target), undefined, "normal", undefined, undefined, undefined, delivery).submit( - new Event("submit"), - ) + await submitInput( + active(state, target), + undefined, + "normal", + undefined, + undefined, + undefined, + () => delivery, + ).submit(new Event("submit")) expect(state.current()).toEqual([{ type: "text", content: "", start: 0, end: 0 }]) expect(statuses).toEqual(["running"]) @@ -630,7 +738,7 @@ describe("Composer submission", () => { () => [], [], undefined, - delivery, + () => delivery, ).submit(new Event("submit")) // The send settles after submit returns; let it and the move that follows it finish. await new Promise((resolve) => setTimeout(resolve, 0)) diff --git a/packages/app/src/composer/submit.ts b/packages/app/src/composer/submit.ts index 05398ad6d78e..4343af00721e 100644 --- a/packages/app/src/composer/submit.ts +++ b/packages/app/src/composer/submit.ts @@ -165,36 +165,7 @@ export function createComposerSubmit(input: ComposerSubmitInput) { const restore = () => restoreSubmission(input, submission, value, comments) if (value.mode === "normal" && !command) { - // The timeline shows a steer, and any switches it makes, as the composer clears; a queued prompt shows in - // the queue once admitted. - if (value.delivery === "steer") - session.handoff.set({ - message: handoffMessage(value), - selection: { - agent: value.selection.agent, - model: { - id: value.selection.model.modelID, - providerID: value.selection.model.providerID, - variant: value.selection.variant, - }, - }, - }) - const optimisticBusy = !input.adapter.working() - - // Like iOS, a steer moves the run's blocking shells and subagents to the background. The move follows - // admission: the steer already waits in the inbox when the moved work ends the step, so that step reads it. - const background = value.delivery === "steer" && !optimisticBusy ? session.background?.() : undefined - - if (optimisticBusy) session.data.session.setStatus(session.id, "running") - - const sending = sendPrompt(session, value, input.adapter.controls().model.selection.trackSessionCommit).then( - () => { - void background?.move() - - return { ok: true as const } - }, - (error) => ({ ok: false as const, error }), - ) + const dispatch = dispatchPrompt(input, session, value) await started.cleanupReady await started.complete?.() @@ -203,12 +174,7 @@ export function createComposerSubmit(input: ComposerSubmitInput) { submission.context.forEach((item) => submission.target().context.remove(item.key)) input.comments.clear() clearSubmission(input, submission) - void sending.then((result) => { - if (!result.ok) - failSubmission(input, session, "prompt", result.error, restore, value.id, () => { - if (optimisticBusy) session.data.session.setStatus(session.id, "idle") - }) - }) + void dispatch.sending.then((result) => dispatch.settle(result, restore)) return } @@ -239,6 +205,56 @@ export function createComposerSubmit(input: ComposerSubmitInput) { return { submit, + async submitQuote(key: string, comment: string) { + if (input.adapter.kind !== "active-session" || held() || submitting.has(input.adapter.state)) return false + const target = input.adapter.state + + const quote = target.context.items().find((item) => item.key === key) + + if (quote?.type !== "quote") return false + const selection = currentSelection(input) + + if (!selection) { + input.notify.missingSelection() + + return false + } + + const session = input.adapter.session() + + const value: ComposerSubmission = { + id: SessionMessage.ID.create(), + mode: "normal", + prompt: [], + context: [{ ...quote, comment, source: quote.source ? { ...quote.source } : undefined }], + text: "", + images: [], + selection, + delivery: input.delivery?.(true) ?? "steer", + } + + submitting.add(input.adapter.state) + + try { + const dispatch = dispatchPrompt(input, session, value) + + const sent = dispatch.settle(await dispatch.sending, () => { + target.context.updateComment(quote.commentID, { comment }) + + return true + }) + + if (!sent) return false + target.context.remove(key) + + if (target.quoteComment.key() === key) target.quoteComment.set() + input.adapter.submitted() + + return true + } finally { + submitting.delete(input.adapter.state) + } + }, held, stop: () => { // Stopping a held send cancels it and leaves the prompt in the composer. @@ -395,6 +411,51 @@ function currentSelection(input: ComposerSubmitInput): ComposerSelection | undef } } +function dispatchPrompt(input: ComposerSubmitInput, session: ComposerSession, value: ComposerSubmission) { + // A steer appears in the timeline before the composer clears; queued prompts appear in the queue on admission. + if (value.delivery === "steer") + session.handoff.set({ + message: handoffMessage(value), + selection: { + agent: value.selection.agent, + model: { + id: value.selection.model.modelID, + providerID: value.selection.model.providerID, + variant: value.selection.variant, + }, + }, + }) + const optimisticBusy = !input.adapter.working() + + const background = value.delivery === "steer" && !optimisticBusy ? session.background?.() : undefined + + if (optimisticBusy) session.data.session.setStatus(session.id, "running") + + const sending = sendPrompt(session, value, input.adapter.controls().model.selection.trackSessionCommit).then( + () => ({ ok: true as const }), + (error) => ({ ok: false as const, error }), + ) + + return { + sending, + // The composer settles after clearing its captured draft; a quote settles before removing only its context. + settle(result: Awaited, restore: () => boolean) { + const admitted = + result.ok || + failSubmission(input, session, "prompt", result.error, restore, value.id, () => { + if (optimisticBusy) session.data.session.setStatus(session.id, "idle") + }) + + if (!admitted) return false + + // Move blocking work only once the steer is admitted, so the next step can read it from the inbox. + void background?.move() + + return true + }, + } +} + function clearSubmission(input: ComposerSubmitInput, submission: ReturnType) { submission.clear() submission.target().mode.set("normal") @@ -654,10 +715,12 @@ function failSubmission( messageID?: string, rollback?: () => void, ) { - if (messageID && session.admitted(messageID)) return + if (messageID && session.admitted(messageID)) return true if (messageID) session.handoff.clear(messageID) rollback?.() restore() input.notify.failed(kind, error) + + return false } diff --git a/packages/app/src/new-session/location/dialog.css b/packages/app/src/new-session/location/dialog.css index f9fa739e752c..bafc1911fa37 100644 --- a/packages/app/src/new-session/location/dialog.css +++ b/packages/app/src/new-session/location/dialog.css @@ -111,10 +111,6 @@ color: var(--v2-text-text-faint); } -.new-session-location-workspace .command-palette-row-icon { - color: var(--v2-icon-icon-base); -} - .new-session-location-workspace .command-palette-row-text > .command-palette-title { max-width: 60%; flex-shrink: 0; diff --git a/packages/app/src/new-session/location/dialog.tsx b/packages/app/src/new-session/location/dialog.tsx index dae5f1022794..0aba30d5fff7 100644 --- a/packages/app/src/new-session/location/dialog.tsx +++ b/packages/app/src/new-session/location/dialog.tsx @@ -3,6 +3,7 @@ import { Dialog, DialogBody } from "@opencode/ui/dialog" import { useDialog } from "@opencode/ui/context/dialog" import { Icon } from "@opencode/ui/icon" import { Keybind } from "@opencode/ui/keybind" +import { MenuScroll } from "@opencode/ui/menu-scroll" import { displayName, getProjectAvatarGenerated, @@ -77,6 +78,7 @@ function NewSessionLocationDialog(props: { const [store, setStore] = createStore<{ drilled?: PromptProject + direct: boolean /** Picking the server to add a project to. */ picking?: boolean /** Browsing server projects this client has not added. */ @@ -88,6 +90,7 @@ function NewSessionLocationDialog(props: { query: "", others: false, drilled: props.drilled, + direct: !!props.drilled, back: props.drilled ? { query: "", active: projectEntryID(props.drilled), others: false } : undefined, }) @@ -281,6 +284,7 @@ function NewSessionLocationDialog(props: { setStore({ drilled: undefined, picking: undefined, + direct: false, others: store.drilled || store.picking ? (store.back?.others ?? false) : false, query: store.drilled || store.picking ? (store.back?.query ?? "") : "", back: undefined, @@ -367,6 +371,14 @@ function NewSessionLocationDialog(props: { return } + if (event.key === "Escape" && store.direct && store.drilled) { + event.preventDefault() + event.stopPropagation() + dialog.close() + + return + } + if ( (store.drilled || store.picking || store.others) && ((event.key === backward && atStart) || (event.key === "Backspace" && !target.value) || event.key === "Escape") @@ -405,7 +417,11 @@ function NewSessionLocationDialog(props: { />