diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml index 8cf87c5d8..20d53e81e 100644 --- a/.github/actions/setup-bun/action.yml +++ b/.github/actions/setup-bun/action.yml @@ -3,12 +3,13 @@ description: "Setup Bun with caching and install dependencies" runs: using: "composite" steps: - - name: Mount Bun Cache - if: ${{ runner.os == 'Linux' }} - uses: useblacksmith/stickydisk@v1 + - name: Cache Bun dependencies + uses: actions/cache@v4 with: - key: ${{ github.repository }}-bun-cache-${{ runner.os }} - path: ~/.bun + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} + restore-keys: | + ${{ runner.os }}-bun- - name: Setup Bun uses: oven-sh/setup-bun@v2 diff --git a/.github/workflows/nix-desktop.yml.disabled b/.github/workflows/nix-desktop.yml.disabled deleted file mode 100644 index 031eff6a6..000000000 --- a/.github/workflows/nix-desktop.yml.disabled +++ /dev/null @@ -1,46 +0,0 @@ -name: nix-desktop - -on: - push: - branches: [dev] - paths: - - "flake.nix" - - "flake.lock" - - "nix/**" - - "packages/app/**" - - "packages/desktop/**" - - ".github/workflows/nix-desktop.yml" - pull_request: - paths: - - "flake.nix" - - "flake.lock" - - "nix/**" - - "packages/app/**" - - "packages/desktop/**" - - ".github/workflows/nix-desktop.yml" - workflow_dispatch: - -jobs: - nix-desktop: - strategy: - fail-fast: false - matrix: - os: - - blacksmith-4vcpu-ubuntu-2404 - - blacksmith-4vcpu-ubuntu-2404-arm - - macos-15-intel - - macos-latest - runs-on: ${{ matrix.os }} - timeout-minutes: 60 - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Setup Nix - uses: nixbuild/nix-quick-install-action@v34 - - - name: Build desktop via flake - run: | - set -euo pipefail - nix --version - nix build .#desktop -L diff --git a/.github/workflows/nix-eval.yml b/.github/workflows/nix-eval.yml new file mode 100644 index 000000000..c76b2c972 --- /dev/null +++ b/.github/workflows/nix-eval.yml @@ -0,0 +1,95 @@ +name: nix-eval + +on: + push: + branches: [dev] + pull_request: + branches: [dev] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + nix-eval: + runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Nix + uses: nixbuild/nix-quick-install-action@v34 + + - name: Evaluate flake outputs (all systems) + run: | + set -euo pipefail + nix --version + + echo "=== Flake metadata ===" + nix flake metadata + + echo "" + echo "=== Flake structure ===" + nix flake show --all-systems + + SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin" + PACKAGES="opencode" + # TODO: move 'desktop' to PACKAGES when #11755 is fixed + OPTIONAL_PACKAGES="desktop" + + echo "" + echo "=== Evaluating packages for all systems ===" + for system in $SYSTEMS; do + echo "" + echo "--- $system ---" + for pkg in $PACKAGES; do + printf " %s: " "$pkg" + if output=$(nix eval ".#packages.$system.$pkg.drvPath" --raw 2>&1); then + echo "✓" + else + echo "✗" + echo "::error::Evaluation failed for packages.$system.$pkg" + echo "$output" + exit 1 + fi + done + done + + echo "" + echo "=== Evaluating optional packages ===" + for system in $SYSTEMS; do + echo "" + echo "--- $system ---" + for pkg in $OPTIONAL_PACKAGES; do + printf " %s: " "$pkg" + if output=$(nix eval ".#packages.$system.$pkg.drvPath" --raw 2>&1); then + echo "✓" + else + echo "✗" + echo "::warning::Evaluation failed for packages.$system.$pkg" + echo "$output" + fi + done + done + + echo "" + echo "=== Evaluating devShells for all systems ===" + for system in $SYSTEMS; do + printf "%s: " "$system" + if output=$(nix eval ".#devShells.$system.default.drvPath" --raw 2>&1); then + echo "✓" + else + echo "✗" + echo "::error::Evaluation failed for devShells.$system.default" + echo "$output" + exit 1 + fi + done + + echo "" + echo "=== All evaluations passed ===" diff --git a/.github/workflows/nix-hashes.yml b/.github/workflows/nix-hashes.yml index 894dbf47b..2529c14c2 100644 --- a/.github/workflows/nix-hashes.yml +++ b/.github/workflows/nix-hashes.yml @@ -6,7 +6,7 @@ permissions: on: workflow_dispatch: push: - branches: [dev] + branches: [dev, beta] paths: - "bun.lock" - "package.json" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f63802ac5..431581f59 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -171,13 +171,23 @@ jobs: GH_TOKEN: ${{ github.token }} GITHUB_RUN_ID: ${{ github.run_id }} + - name: Resolve tauri portable SHA + if: contains(matrix.settings.host, 'ubuntu') + run: echo "TAURI_PORTABLE_SHA=$(git ls-remote https://github.com/tauri-apps/tauri.git refs/heads/feat/truly-portable-appimage | cut -f1)" >> "$GITHUB_ENV" + # Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released - name: Install tauri-cli from portable appimage branch + uses: taiki-e/cache-cargo-install-action@v3 if: contains(matrix.settings.host, 'ubuntu') - run: | - cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force - echo "Installed tauri-cli version:" - cargo tauri --version + with: + tool: tauri-cli + git: https://github.com/tauri-apps/tauri + # branch: feat/truly-portable-appimage + rev: ${{ env.TAURI_PORTABLE_SHA }} + + - name: Show tauri-cli version + if: contains(matrix.settings.host, 'ubuntu') + run: cargo tauri --version - name: Build and upload artifacts uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md index 5d1147a88..a77b92737 100644 --- a/.opencode/agent/triage.md +++ b/.opencode/agent/triage.md @@ -1,7 +1,7 @@ --- mode: primary hidden: true -model: opencode/claude-haiku-4-5 +model: opencode/minimax-m2.5 color: "#44BA81" tools: "*": false @@ -12,6 +12,8 @@ You are a triage agent responsible for triaging github issues. Use your github-triage tool to triage issues. +This file is the source of truth for ownership/routing rules. + ## Labels ### windows @@ -43,12 +45,34 @@ Desktop app issues: **Only** add if the issue explicitly mentions nix. +If the issue does not mention nix, do not add nix. + +If the issue mentions nix, assign to `rekram1-node`. + #### zen **Only** add if the issue mentions "zen" or "opencode zen" or "opencode black". If the issue doesn't have "zen" or "opencode black" in it then don't add zen label +#### core + +Use for core server issues in `packages/opencode/`, excluding `packages/opencode/src/cli/cmd/tui/`. + +Examples: + +- LSP server behavior +- Harness behavior (agent + tools) +- Feature requests for server behavior +- Agent context construction +- API endpoints +- Provider integration issues +- New, broken, or poor-quality models + +#### acp + +If the issue mentions acp support, assign acp label. + #### docs Add if the issue requests better documentation or docs updates. @@ -66,13 +90,51 @@ TUI issues potentially caused by our underlying TUI library: When assigning to people here are the following rules: -adamdotdev: -ONLY assign adam if the issue will have the "desktop" label. +Desktop / Web: +Use for desktop-labeled issues only. -fwang: -ONLY assign fwang if the issue will have the "zen" label. +- adamdotdevin +- iamdavidhill +- Brendonovich +- nexxeln -jayair: -ONLY assign jayair if the issue will have the "docs" label. +Zen: +ONLY assign if the issue will have the "zen" label. -In all other cases use best judgment. Avoid assigning to kommander needlessly, when in doubt assign to rekram1-node. +- fwang +- MrMushrooooom + +TUI (`packages/opencode/src/cli/cmd/tui/...`): + +- thdxr for TUI UX/UI product decisions and interaction flow +- kommander for OpenTUI engine issues: rendering artifacts, keybind handling, terminal compatibility, SSH behavior, and low-level perf bottlenecks +- rekram1-node for TUI bugs that are not clearly OpenTUI engine issues + +Core (`packages/opencode/...`, excluding TUI subtree): + +- thdxr for sqlite/snapshot/memory bugs and larger architectural core features +- jlongster for opencode server + API feature work (tool currently remaps jlongster -> thdxr until assignable) +- rekram1-node for harness issues, provider issues, and other bug-squashing + +For core bugs that do not clearly map, either thdxr or rekram1-node is acceptable. + +Docs: + +- R44VC0RP + +Windows: + +- Hona (assign any issue that mentions Windows or is likely Windows-specific) + +Determinism rules: + +- If title + body does not contain "zen", do not add the "zen" label +- If "nix" label is added but title + body does not mention nix/nixos, the tool will drop "nix" +- If title + body mentions nix/nixos, assign to `rekram1-node` +- If "desktop" label is added, the tool will override assignee and randomly pick one Desktop / Web owner + +In all other cases, choose the team/section with the most overlap with the issue and assign a member from that team at random. + +ACP: + +- rekram1-node (assign any acp issues to rekram1-node) diff --git a/.opencode/tool/github-triage.ts b/.opencode/tool/github-triage.ts index 1e216f1c8..ed80f49d5 100644 --- a/.opencode/tool/github-triage.ts +++ b/.opencode/tool/github-triage.ts @@ -1,8 +1,22 @@ /// -// import { Octokit } from "@octokit/rest" import { tool } from "@opencode-ai/plugin" import DESCRIPTION from "./github-triage.txt" +const TEAM = { + desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"], + zen: ["fwang", "MrMushrooooom"], + tui: ["thdxr", "kommander", "rekram1-node"], + core: ["thdxr", "rekram1-node", "jlongster"], + docs: ["R44VC0RP"], + windows: ["Hona"], +} as const + +const ASSIGNEES = [...new Set(Object.values(TEAM).flat())] + +function pick(items: readonly T[]) { + return items[Math.floor(Math.random() * items.length)]! +} + function getIssueNumber(): number { const issue = parseInt(process.env.ISSUE_NUMBER ?? "", 10) if (!issue) throw new Error("ISSUE_NUMBER env var not set") @@ -29,60 +43,69 @@ export default tool({ description: DESCRIPTION, args: { assignee: tool.schema - .enum(["thdxr", "adamdotdevin", "rekram1-node", "fwang", "jayair", "kommander"]) + .enum(ASSIGNEES as [string, ...string[]]) .describe("The username of the assignee") .default("rekram1-node"), labels: tool.schema - .array(tool.schema.enum(["nix", "opentui", "perf", "desktop", "zen", "docs", "windows"])) + .array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"])) .describe("The labels(s) to add to the issue") .default([]), }, async execute(args) { const issue = getIssueNumber() - // const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }) const owner = "anomalyco" const repo = "opencode" const results: string[] = [] + let labels = [...new Set(args.labels.map((x) => (x === "desktop" ? "web" : x)))] + const web = labels.includes("web") + const text = `${process.env.ISSUE_TITLE ?? ""}\n${process.env.ISSUE_BODY ?? ""}`.toLowerCase() + const zen = /\bzen\b/.test(text) || text.includes("opencode black") + const nix = /\bnix(os)?\b/.test(text) - if (args.assignee === "adamdotdevin" && !args.labels.includes("desktop")) { - throw new Error("Only desktop issues should be assigned to adamdotdevin") + if (labels.includes("nix") && !nix) { + labels = labels.filter((x) => x !== "nix") + results.push("Dropped label: nix (issue does not mention nix)") } - if (args.assignee === "fwang" && !args.labels.includes("zen")) { - throw new Error("Only zen issues should be assigned to fwang") + const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee + + if (labels.includes("zen") && !zen) { + throw new Error("Only add the zen label when issue title/body contains 'zen'") } - if (args.assignee === "kommander" && !args.labels.includes("opentui")) { + if (web && !nix && !(TEAM.desktop as readonly string[]).includes(assignee)) { + throw new Error("Web issues must be assigned to adamdotdevin, iamdavidhill, Brendonovich, or nexxeln") + } + + if ((TEAM.zen as readonly string[]).includes(assignee) && !labels.includes("zen")) { + throw new Error("Only zen issues should be assigned to fwang or MrMushrooooom") + } + + if (assignee === "Hona" && !labels.includes("windows")) { + throw new Error("Only windows issues should be assigned to Hona") + } + + if (assignee === "R44VC0RP" && !labels.includes("docs")) { + throw new Error("Only docs issues should be assigned to R44VC0RP") + } + + if (assignee === "kommander" && !labels.includes("opentui")) { throw new Error("Only opentui issues should be assigned to kommander") } - // await octokit.rest.issues.addAssignees({ - // owner, - // repo, - // issue_number: issue, - // assignees: [args.assignee], - // }) await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, { method: "POST", - body: JSON.stringify({ assignees: [args.assignee] }), + body: JSON.stringify({ assignees: [assignee] }), }) - results.push(`Assigned @${args.assignee} to issue #${issue}`) - - const labels: string[] = args.labels.map((label) => (label === "desktop" ? "web" : label)) + results.push(`Assigned @${assignee} to issue #${issue}`) if (labels.length > 0) { - // await octokit.rest.issues.addLabels({ - // owner, - // repo, - // issue_number: issue, - // labels, - // }) await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/labels`, { method: "POST", body: JSON.stringify({ labels }), }) - results.push(`Added labels: ${args.labels.join(", ")}`) + results.push(`Added labels: ${labels.join(", ")}`) } return results.join("\n") diff --git a/.opencode/tool/github-triage.txt b/.opencode/tool/github-triage.txt index ae47cf4cb..4369ed235 100644 --- a/.opencode/tool/github-triage.txt +++ b/.opencode/tool/github-triage.txt @@ -1,88 +1,6 @@ Use this tool to assign and/or label a GitHub issue. -You can assign the following users: -- thdxr -- adamdotdevin -- fwang -- jayair -- kommander -- rekram1-node +Choose labels and assignee using the current triage policy and ownership rules. +Pick the most fitting labels for the issue and assign one owner. - -You can use the following labels: -- nix -- opentui -- perf -- web -- zen -- docs - -Always try to assign an issue, if in doubt, assign rekram1-node to it. - -## Breakdown of responsibilities: - -### thdxr - -Dax is responsible for managing core parts of the application, for large feature requests, api changes, or things that require significant changes to the codebase assign him. - -This relates to OpenCode server primarily but has overlap with just about anything - -### adamdotdevin - -Adam is responsible for managing the Desktop/Web app. If there is an issue relating to the desktop app or `opencode web` command. Assign him. - - -### fwang - -Frank is responsible for managing Zen, if you see complaints about OpenCode Zen, maybe it's the dashboard, the model quality, billing issues, etc. Assign him to the issue. - -### jayair - -Jay is responsible for documentation. If there is an issue relating to documentation assign him. - -### kommander - -Sebastian is responsible for managing an OpenTUI (a library for building terminal user interfaces). OpenCode's TUI is built with OpenTUI. If there are issues about: -- random characters on screen -- keybinds not working on different terminals -- general terminal stuff -Then assign the issue to Him. - -### rekram1-node - -ALL BUGS SHOULD BE assigned to rekram1-node unless they have the `opentui` label. - -Assign Aiden to an issue as a catch all, if you can't assign anyone else. Most of the time this will be bugs/polish things. -If no one else makes sense to assign, assign rekram1-node to it. - -Always assign to aiden if the issue mentions "acp", "zed", or model performance issues - -## Breakdown of Labels: - -### nix - -Any issue that mentions nix, or nixos should have a nix label - -### opentui - -Anything relating to the TUI itself should have an opentui label - -### perf - -Anything related to slow performance, high ram, high cpu usage, or any other performance related issue should have a perf label - -### desktop - -Anything related to `opencode web` command or the desktop app should have a desktop label. Never add this label for anything terminal/tui related - -### zen - -Anything related to OpenCode Zen, billing, or model quality from Zen should have a zen label - -### docs - -Anything related to the documentation should have a docs label - -### windows - -Use for any issue that involves the windows OS +If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random. diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 000000000..a3a5e1e2b --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,9 @@ +{ + "format_on_save": "on", + "formatter": { + "external": { + "command": "bunx", + "arguments": ["prettier", "--stdin-filepath", "{buffer_path}"] + } + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4bec009ef..2ae3fc6f2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,11 @@ If you are unsure if a PR would be accepted, feel free to ask a maintainer or lo Want to take on an issue? Leave a comment and a maintainer may assign it to you unless it is something we are already working on. +## Adding New Providers + +New providers shouldn't require many if ANY code changes, but if you want to add support for a new provider first make a PR to: +https://github.com/anomalyco/models.dev + ## Developing OpenCode - Requirements: Bun 1.3+ diff --git a/SECURITY.md b/SECURITY.md index 93c7341ce..e7e59f4a2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,5 +1,11 @@ # Security +## IMPORTANT + +We do not accept AI generated security reports. We receive a large number of +these and we absolutely do not have the resources to review them all. If you +submit one that will be an automatic ban from the project. + ## Threat Model ### Overview diff --git a/bun.lock b/bun.lock index 07e239a78..bd340ea6e 100644 --- a/bun.lock +++ b/bun.lock @@ -14,10 +14,11 @@ "devDependencies": { "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", + "@types/mime-types": "3.0.1", "husky": "9.1.7", "prettier": "3.6.2", "semver": "^7.6.0", - "sst": "3.17.23", + "sst": "3.18.10", "turbo": "2.5.6", }, }, @@ -288,7 +289,7 @@ "@ai-sdk/vercel": "1.0.33", "@ai-sdk/xai": "2.0.51", "@clack/prompts": "1.0.0-alpha.1", - "@gitlab/gitlab-ai-provider": "3.5.1", + "@gitlab/gitlab-ai-provider": "3.6.0", "@gitlab/opencode-gitlab-auth": "1.3.3", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", @@ -324,6 +325,7 @@ "hono-openapi": "catalog:", "ignore": "7.0.5", "jsonc-parser": "3.3.1", + "mime-types": "3.0.2", "minimatch": "10.0.3", "open": "10.1.2", "opentui-spinner": "0.0.6", @@ -356,6 +358,7 @@ "@tsconfig/bun": "catalog:", "@types/babel__core": "7.20.5", "@types/bun": "catalog:", + "@types/mime-types": "3.0.1", "@types/turndown": "5.0.5", "@types/yargs": "17.0.33", "@typescript/native-preview": "catalog:", @@ -989,7 +992,7 @@ "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], - "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.5.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-I8+EGdUeKmGJSjAdFobHtqpxM9Fm00w0j7NJbtln/D/XQ1SKEGoZIuqJko4v0pV2mkhGUIs7qezljH/2kbXovA=="], + "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="], "@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="], @@ -1609,7 +1612,7 @@ "@smithy/config-resolver": ["@smithy/config-resolver@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ=="], - "@smithy/core": ["@smithy/core@3.23.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg=="], + "@smithy/core": ["@smithy/core@3.23.2", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-HaaH4VbGie4t0+9nY3tNBRSxVTr96wzIqexUa6C2qx3MPePAuz7lIxPxYtt1Wc//SPfJLNoZJzfdt0B6ksj2jA=="], "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw=="], @@ -1639,9 +1642,9 @@ "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.14", "", { "dependencies": { "@smithy/core": "^3.23.0", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.16", "", { "dependencies": { "@smithy/core": "^3.23.2", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-L5GICFCSsNhbJ5JSKeWFGFy16Q2OhoBizb3X2DrxaJwXSEujVvjG9Jt386dpQn2t7jINglQl0b4K/Su69BdbMA=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.31", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.33", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-jLqZOdJhtIL4lnA9hXnAG6GgnJlo1sD3FqsTxm9wSfjviqgWesY/TMBVnT84yr4O0Vfe0jWoXlfFbzsBVph3WA=="], "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ=="], @@ -1665,7 +1668,7 @@ "@smithy/signature-v4": ["@smithy/signature-v4@5.3.8", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.11.3", "", { "dependencies": { "@smithy/core": "^3.23.0", "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" } }, "sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.11.5", "", { "dependencies": { "@smithy/core": "^3.23.2", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" } }, "sha512-xixwBRqoeP2IUgcAl3U9dvJXc+qJum4lzo3maaJxifsZxKUYLfVfCXvhT4/jD01sRrHg5zjd1cw2Zmjr4/SuKQ=="], "@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="], @@ -1681,9 +1684,9 @@ "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.30", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.32", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-092sjYfFMQ/iaPH798LY/OJFBcYu0sSK34Oy9vdixhsU36zlZu8OcYjF3TD4e2ARupyK7xaxPXl+T0VIJTEkkg=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.33", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.35", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-miz/ggz87M8VuM29y7jJZMYkn7+IErM5p5UgKIf8OtqVs/h2bXr1Bt3uTsREsI/4nK8a0PQERbAPsVPVNIsG7Q=="], "@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw=="], @@ -1917,6 +1920,8 @@ "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/mime-types": ["@types/mime-types@3.0.1", "", {}, "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], "@types/mssql": ["@types/mssql@9.1.9", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A=="], @@ -1983,7 +1988,7 @@ "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "x64" }, "sha512-5l51HlXjX7lXwo65DEl1IaCFLjmkMtL6K3NrSEamPNeNTtTQwZRa3pQ9V65dCglnnCQ0M3+VF1RqzC7FU0iDKg=="], - "@typescript/vfs": ["@typescript/vfs@1.6.2", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g=="], + "@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="], "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="], @@ -2049,7 +2054,7 @@ "ai-gateway-provider": ["ai-gateway-provider@2.3.1", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.19", "ai": "^5.0.116" }, "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^3.0.71", "@ai-sdk/anthropic": "^2.0.56", "@ai-sdk/azure": "^2.0.90", "@ai-sdk/cerebras": "^1.0.33", "@ai-sdk/cohere": "^2.0.21", "@ai-sdk/deepgram": "^1.0.21", "@ai-sdk/deepseek": "^1.0.32", "@ai-sdk/elevenlabs": "^1.0.21", "@ai-sdk/fireworks": "^1.0.30", "@ai-sdk/google": "^2.0.51", "@ai-sdk/google-vertex": "3.0.90", "@ai-sdk/groq": "^2.0.33", "@ai-sdk/mistral": "^2.0.26", "@ai-sdk/openai": "^2.0.88", "@ai-sdk/perplexity": "^2.0.22", "@ai-sdk/xai": "^2.0.42", "@openrouter/ai-sdk-provider": "^1.5.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^1.0.29" } }, "sha512-PqI6TVNEDNwr7kOhy7XUGnA8XJB1SpeA9aLqGjr0CyWkKgH+y+ofPm8MZGZ74DOwVejDF+POZq0Qs9jKEKUeYg=="], - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], @@ -2131,11 +2136,11 @@ "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - "b4a": ["b4a@1.7.4", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug=="], + "b4a": ["b4a@1.7.5", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-iEsKNwDh1wiWTps1/hdkNdmBgDlDVZP5U57ZVOlt+dNFqpc/lpPouCIxZw+DYBgc4P9NDfIZMPNR4CHNhzwLIA=="], "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], - "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.3", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w=="], + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.5", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-8TFKemVLDYezqqv4mWz+PhRrkryTzivTGu0twyLrOkVZ0P63COx2Y04eVsUjFlwSOXui1z3P3Pn209dokWnirg=="], "babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="], @@ -2211,13 +2216,13 @@ "bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="], - "bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eDgLN9teKTfmvrCqgwwmWNsNszxYs7IZdCqk0S1DCarvMhr4wcajoSBlA/nQA0/owwLduPTS8xxCnQp4/N/gDg=="], + "bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qM7W5IaFpWYGPDcNiQ8DOng3noQ97gxpH2MFH1mGsdKwI0T4oy++egSh5Z7s6AQx8WKgc9GzAsTUM4KZkFdacw=="], - "bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-X+PjwJUWenUmdQBP8EtdItMyieQ6Nlpn+BH518oaouDiSnWj5+b0Y7DNDZJq7Ezom4EaxmqL/uGYZK3aCQ7CXg=="], + "bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-oVoIsme27pcXB68YxnQSAgdNGCa4A3PGWYIBUewOh9VnJaoik4JenGb5Yy+svGE+ETFhQXV9nhHqgMPsDRrO6A=="], - "bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zMLs2YIGB+/jxrYFXaFhVKX/GBt05UTF45lc9srcHc9JXGjEj+12CIo1CHLTAWatXMTqt0Jsu6ukWEoWVT/ayA=="], + "bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-+SYt09k+xDEl/GfcU7L1zdNgm7IlvAFKV5Xl/auBwuprKG5UwXNhjRlRAWfhTMCUZWN+NDf8E+ZQx0cQi9K2/g=="], - "bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Z5yAK28xrcm8Wb5k7TZ8FJKpOI/r+aVCRdlHYAqI2SDJFN3nD4mJs900X6kNVmG/xFzb5yOuKVYWGg+6ZXWbyA=="], + "bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-zvnUl4EAsQbKsmZVu+lEJcH8axQ7MiCfqg2OmnHd6uw1THABmHaX0GbpKiHshdgadNN2Nf+4zDyTJB5YMcAdrA=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -2237,7 +2242,7 @@ "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001769", "", {}, "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -2471,7 +2476,7 @@ "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], - "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], @@ -2707,7 +2712,7 @@ "h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="], - "happy-dom": ["happy-dom@20.6.1", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^6.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-+0vhESXXhFwkdjZnJ5DlmJIfUYGgIEEjzIjB+aKJbFuqlvvKyOi+XkI1fYbgYR9QCxG5T08koxsQ6HrQfa5gCQ=="], + "happy-dom": ["happy-dom@20.6.2", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-Xk/Y0cuq9ngN/my8uvK4gKoyDl6sBKkIl8A/hJ0IabZVH7E5SJLHNE7uKRPVmSrQbhJaLIHTEcvTct4GgNtsRA=="], "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], @@ -2919,7 +2924,7 @@ "is-whitespace": ["is-whitespace@0.3.0", "", {}, "sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg=="], - "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], "is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="], @@ -3497,7 +3502,7 @@ "punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="], - "qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -3759,23 +3764,23 @@ "srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="], - "sst": ["sst@3.17.23", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.23", "sst-darwin-x64": "3.17.23", "sst-linux-arm64": "3.17.23", "sst-linux-x64": "3.17.23", "sst-linux-x86": "3.17.23", "sst-win32-arm64": "3.17.23", "sst-win32-x64": "3.17.23", "sst-win32-x86": "3.17.23" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-TwKgUgDnZdc1Swe+bvCNeyO4dQnYz5cTodMpYj3jlXZdK9/KNz0PVxT1f0u5E76i1pmilXrUBL/f7iiMPw4RDg=="], + "sst": ["sst@3.18.10", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.18.10", "sst-darwin-x64": "3.18.10", "sst-linux-arm64": "3.18.10", "sst-linux-x64": "3.18.10", "sst-linux-x86": "3.18.10", "sst-win32-arm64": "3.18.10", "sst-win32-x64": "3.18.10", "sst-win32-x86": "3.18.10" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-SY+ldeJ9K5E9q+DhjXA3e2W3BEOzBwkE3IyLSD71uA3/5nRhUAST31iOWEpW36LbIvSQ9uOVDFcebztoLJ8s7w=="], - "sst-darwin-arm64": ["sst-darwin-arm64@3.17.23", "", { "os": "darwin", "cpu": "arm64" }, "sha512-R6kvmF+rUideOoU7KBs2SdvrIupoE+b+Dor/eq9Uo4Dojj7KvYDZI/EDm8sSCbbcx/opiWeyNqKtlnLEdCxE6g=="], + "sst-darwin-arm64": ["sst-darwin-arm64@3.18.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3MwIpMZhhdZKDqLp9ZQNlwkWix5+q+N0PWstuTomYwgZOxCCe6u9IIsoIszSk+GAJJN/jvGZyLiXKeV4iiQvw=="], - "sst-darwin-x64": ["sst-darwin-x64@3.17.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-WW4P1S35iYCifQXxD+sE3wuzcN+LHLpuKMaNoaBqEcWGZnH3IPaDJ7rpLF0arkDAo/z3jZmWWzOCkr0JuqJ8vQ=="], + "sst-darwin-x64": ["sst-darwin-x64@3.18.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-nQ0jMKkPOa+kj6Ygz8+kYhBua/vgNTLkd+4r8NSmk7v+Zs78lKnx3T//kEzS0yik6Q6QwGfokwrTcA1Jii2xSw=="], - "sst-linux-arm64": ["sst-linux-arm64@3.17.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-TjtNqgIh7RlAWgPLFCAt0mXvIB+J7WjmRvIRrAdX0mXsndOiBJ/DMOgXSLVsIWHCfPj8MIEot/hWpnJgXgIeag=="], + "sst-linux-arm64": ["sst-linux-arm64@3.18.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-mj9VNj3SvLS+HaXx2PhCX0aTA7CwJNoM6JhRc0s/zCilqchcvqDjbhpYBJO4brEPv6aOaaa7T3WvIQqtYauK4Q=="], - "sst-linux-x64": ["sst-linux-x64@3.17.23", "", { "os": "linux", "cpu": "x64" }, "sha512-qdqJiEbYfCjZlI3F/TA6eoIU7JXVkEEI/UMILNf2JWhky0KQdCW2Xyz+wb6c0msVJCWdUM/uj+1DaiP2eXvghw=="], + "sst-linux-x64": ["sst-linux-x64@3.18.10", "", { "os": "linux", "cpu": "x64" }, "sha512-7iy1Eq2eqnT9Ag/8OVgC04vRjV7AAQyf/BvzLc+6Sz+GvRiKA8VEuPnbXNYQF+NIvEqsawfcd7MknSTtImpsvQ=="], - "sst-linux-x86": ["sst-linux-x86@3.17.23", "", { "os": "linux", "cpu": "none" }, "sha512-aGmUujIvoNlmAABEGsOgfY1rxD9koC6hN8bnTLbDI+oI/u/zjHYh50jsbL0p3TlaHpwF/lxP3xFSuT6IKp+KgA=="], + "sst-linux-x86": ["sst-linux-x86@3.18.10", "", { "os": "linux", "cpu": "none" }, "sha512-77qZSuPZeQ5bdRCiq1pQEdY8EcGNHboKrx4P2yFid2FBDKJsXxOXtIxJdloyx+ljBn0+nxl/g040QBmXxdc9tA=="], - "sst-win32-arm64": ["sst-win32-arm64@3.17.23", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZxdkGqYDrrZGz98rijDCN+m5yuCcwD6Bc9/6hubLsvdpNlVorUqzpg801Ec97xSK0nIC9g6pNiRyxAcsQQstUg=="], + "sst-win32-arm64": ["sst-win32-arm64@3.18.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-aY+FhMxvYs8crlrKALpLn/kKmud8YQj6LkMHsrOAAIJhfNyxhCja2vrYQaY+bcqdsS5W2LMVcS2hyaMqKXZKcg=="], - "sst-win32-x64": ["sst-win32-x64@3.17.23", "", { "os": "win32", "cpu": "x64" }, "sha512-yc9cor4MS49Ccy2tQCF1tf6M81yLeSGzGL+gjhUxpVKo2pN3bxl3w70eyU/mTXSEeyAmG9zEfbt6FNu4sy5cUA=="], + "sst-win32-x64": ["sst-win32-x64@3.18.10", "", { "os": "win32", "cpu": "x64" }, "sha512-rY+yJXOpG+P5xXnaQRpCvBK2zwwLhjzpYidGkp6F+cGgiVdh2Wre/CIQNRaVHr20ncj8lLe/RsHWa9QCNM48jg=="], - "sst-win32-x86": ["sst-win32-x86@3.17.23", "", { "os": "win32", "cpu": "none" }, "sha512-DIp3s54IpNAfdYjSRt6McvkbEPQDMxUu6RUeRAd2C+FcTJgTloon/ghAPQBaDgu2VoVgymjcJARO/XyfKcCLOQ=="], + "sst-win32-x86": ["sst-win32-x86@3.18.10", "", { "os": "win32", "cpu": "none" }, "sha512-pq8SmV0pIjBFMY6DraUZ4akyTxHnfjIKCRbBLdMxFUZK8TzA1NK2YdjRt1AwrgXRYGRyctrz/mt4WyO0SMOVQQ=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -3841,19 +3846,19 @@ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], - "tar": ["tar@7.5.7", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ=="], + "tar": ["tar@7.5.9", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg=="], "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], "tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="], - "tedious": ["tedious@19.2.1", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.5", "@types/node": ">=18", "bl": "^6.1.4", "iconv-lite": "^0.7.0", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA=="], + "tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="], "terracotta": ["terracotta@1.1.0", "", { "dependencies": { "solid-use": "^0.9.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww=="], "terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="], - "text-decoder": ["text-decoder@1.2.4", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-mzlffA3tBNhziEHPK5L5InZg1d/ElNIpJhnhbDRNUtem/edZcJ5zg5FgwKKKOyklxk+6Jt+TrSu83musmvrDlg=="], + "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], @@ -3961,7 +3966,7 @@ "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], - "undici": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="], + "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], @@ -4127,7 +4132,7 @@ "xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="], - "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], + "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], @@ -4277,8 +4282,6 @@ "@azure/core-http/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], - "@azure/core-http/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], - "@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], "@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], @@ -4299,7 +4302,7 @@ "@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], - "@gitlab/gitlab-ai-provider/openai": ["openai@6.21.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-26dQFi76dB8IiN/WKGQOV+yKKTTlRCxQjoi2WLt0kMcH8pvxVyvfdBDkld5GTl7W1qvBpwVOtFcsqktj3fBRpA=="], + "@gitlab/gitlab-ai-provider/openai": ["openai@6.22.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-7Yvy17F33Bi9RutWbsaYt5hJEEJ/krRPOrwan+f9aCPuMat1WVsb2VNSII5W1EksKT6fF69TG/xj4XzodK3JZw=="], "@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -4501,7 +4504,9 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="], + "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.63", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zXlUPCkumnvp8lWS9VFcen/MLF6CL/t1zAKDhpobYj9y/nmylQrKtRvn3RwH871Wd3dF3KYEUXd6M2c6dfCKOA=="], + + "ai-gateway-provider/@ai-sdk/google": ["@ai-sdk/google@2.0.53", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ccCxr5mrd3AC2CjLq4e1ST7+UiN5T2Pdmgi0XdWM3QohmNBwUQ/RBG7BvL+cB/ex/j6y64tkMmpYz9zBw/SEFQ=="], "ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.90", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C9MLe1KZGg1ZbupV2osygHtL5qngyCDA6ATatunyfTbIe8TXKG8HGni/3O6ifbnI5qxTidIn150Ox7eIFZVMYg=="], @@ -4509,6 +4514,8 @@ "ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="], + "ai-gateway-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -4529,6 +4536,8 @@ "aws-sdk/events": ["events@1.1.1", "", {}, "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw=="], + "aws-sdk/xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], @@ -4539,6 +4548,10 @@ "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "body-parser/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], + + "buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="], "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], @@ -4573,6 +4586,8 @@ "express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + "express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], + "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -4581,7 +4596,7 @@ "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "glob/minimatch": ["minimatch@10.2.0", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w=="], + "glob/minimatch": ["minimatch@10.2.1", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="], "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -4619,8 +4634,6 @@ "mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], - "mssql/tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="], - "nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], @@ -4649,10 +4662,10 @@ "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], - "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], @@ -4709,6 +4722,8 @@ "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -4725,6 +4740,8 @@ "unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + "unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], "vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], @@ -4745,6 +4762,8 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "xml2js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], + "yaml-language-server/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], @@ -5005,8 +5024,6 @@ "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], - "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="], "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8PK6u4sGE/kXebd7ZkTp+0aya4kNqzoqpS5m7cHY2NfTK6fhPc6GNvE+MZIZIoHQTp5ed86wGBdeBPpFaaUtyg=="], @@ -5015,7 +5032,7 @@ "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="], - "ai-gateway-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -5035,6 +5052,8 @@ "astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + "aws-sdk/xml2js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], + "babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], "babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], @@ -5069,8 +5088,6 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "mssql/tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], diff --git a/github/sst-env.d.ts b/github/sst-env.d.ts index f742a1200..3b8cffd4f 100644 --- a/github/sst-env.d.ts +++ b/github/sst-env.d.ts @@ -2,6 +2,7 @@ /* tslint:disable */ /* eslint-disable */ /* deno-fmt-ignore-file */ +/* biome-ignore-all lint: auto-generated */ /// diff --git a/infra/console.ts b/infra/console.ts index 4e5a14b04..3f3c2b8d9 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -145,6 +145,16 @@ const ZEN_MODELS = [ new sst.Secret("ZEN_MODELS18"), new sst.Secret("ZEN_MODELS19"), new sst.Secret("ZEN_MODELS20"), + new sst.Secret("ZEN_MODELS21"), + new sst.Secret("ZEN_MODELS22"), + new sst.Secret("ZEN_MODELS23"), + new sst.Secret("ZEN_MODELS24"), + new sst.Secret("ZEN_MODELS25"), + new sst.Secret("ZEN_MODELS26"), + new sst.Secret("ZEN_MODELS27"), + new sst.Secret("ZEN_MODELS28"), + new sst.Secret("ZEN_MODELS29"), + new sst.Secret("ZEN_MODELS30"), ] const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY") const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY") @@ -204,9 +214,9 @@ new sst.cloudflare.x.SolidStart("Console", { }, transform: { server: { + placement: { region: "aws:us-east-1" }, transform: { worker: { - placement: { mode: "smart" }, tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }], }, }, diff --git a/nix/hashes.json b/nix/hashes.json index 3fa1455fc..d0e314a74 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-C3WIEER2XgzO85wk2sp3BzQ6dknW026zslD8nKZjo2U=", - "aarch64-linux": "sha256-+tTJHZMZ/+8fAjI/1fUTuca8J2MZfB+5vhBoZ7jgqcE=", - "aarch64-darwin": "sha256-vS82puFGBBToxyIBa8Zi0KLKdJYr64T6HZL2rL32mH8=", - "x86_64-darwin": "sha256-Tr8JMTCxV6WVt3dXV7iq3PNCm2Cn+RXAbU9+o7pKKV0=" + "x86_64-linux": "sha256-7y6gQyIxyrdp2DaG/0oOEpuL+1n9oa8arUn1CuDiDhA=", + "aarch64-linux": "sha256-7dnHO2WqQZ9A8cG3EC8p7408YR9n2F5C6DG5rNWHqNY=", + "aarch64-darwin": "sha256-jxjhnVfE61RVOHaWvDO4mGLk6guQ8jHeXv/pbu5nbaE=", + "x86_64-darwin": "sha256-22yM4FEtVxGWRug6H0rKog86Q/cYE3QsADrRbLeJKVQ=" } } diff --git a/package.json b/package.json index 5d9320505..f1ba10269 100644 --- a/package.json +++ b/package.json @@ -69,10 +69,11 @@ "devDependencies": { "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", + "@types/mime-types": "3.0.1", "husky": "9.1.7", "prettier": "3.6.2", "semver": "^7.6.0", - "sst": "3.17.23", + "sst": "3.18.10", "turbo": "2.5.6" }, "dependencies": { diff --git a/packages/app/e2e/commands/input-focus.spec.ts b/packages/app/e2e/commands/input-focus.spec.ts new file mode 100644 index 000000000..4ba1aa3e6 --- /dev/null +++ b/packages/app/e2e/commands/input-focus.spec.ts @@ -0,0 +1,15 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" + +test("ctrl+l focuses the prompt", async ({ page, gotoSession }) => { + await gotoSession() + + const prompt = page.locator(promptSelector) + await expect(prompt).toBeVisible() + + await page.locator("main").click({ position: { x: 5, y: 5 } }) + await expect(prompt).not.toBeFocused() + + await page.keyboard.press("Control+L") + await expect(prompt).toBeFocused() +}) diff --git a/packages/app/e2e/commands/panels.spec.ts b/packages/app/e2e/commands/panels.spec.ts new file mode 100644 index 000000000..58c1f0a9a --- /dev/null +++ b/packages/app/e2e/commands/panels.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from "../fixtures" +import { modKey } from "../utils" + +const expanded = async (el: { getAttribute: (name: string) => Promise }) => { + const value = await el.getAttribute("aria-expanded") + if (value !== "true" && value !== "false") throw new Error(`Expected aria-expanded to be true|false, got: ${value}`) + return value === "true" +} + +test("review panel can be toggled via keybind", async ({ page, gotoSession }) => { + await gotoSession() + + const treeToggle = page.getByRole("button", { name: "Toggle file tree" }).first() + await expect(treeToggle).toBeVisible() + if (await expanded(treeToggle)) await treeToggle.click() + await expect(treeToggle).toHaveAttribute("aria-expanded", "false") + + const reviewToggle = page.getByRole("button", { name: "Toggle review" }).first() + await expect(reviewToggle).toBeVisible() + if (await expanded(reviewToggle)) await reviewToggle.click() + await expect(reviewToggle).toHaveAttribute("aria-expanded", "false") + await expect(page.locator("#review-panel")).toHaveCount(0) + + await page.keyboard.press(`${modKey}+Shift+R`) + await expect(reviewToggle).toHaveAttribute("aria-expanded", "true") + await expect(page.locator("#review-panel")).toBeVisible() + + await page.keyboard.press(`${modKey}+Shift+R`) + await expect(reviewToggle).toHaveAttribute("aria-expanded", "false") + await expect(page.locator("#review-panel")).toHaveCount(0) +}) diff --git a/packages/app/e2e/commands/tab-close.spec.ts b/packages/app/e2e/commands/tab-close.spec.ts new file mode 100644 index 000000000..981ee561e --- /dev/null +++ b/packages/app/e2e/commands/tab-close.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" +import { modKey } from "../utils" + +test("mod+w closes the active file tab", async ({ page, gotoSession }) => { + await gotoSession() + + await page.locator(promptSelector).click() + await page.keyboard.type("/open") + await expect(page.locator('[data-slash-id="file.open"]').first()).toBeVisible() + await page.keyboard.press("Enter") + + const dialog = page + .getByRole("dialog") + .filter({ has: page.getByPlaceholder(/search files/i) }) + .first() + await expect(dialog).toBeVisible() + + await dialog.getByRole("textbox").first().fill("package.json") + const item = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first() + await expect(item).toBeVisible({ timeout: 30_000 }) + await item.click() + await expect(dialog).toHaveCount(0) + + const tab = page.getByRole("tab", { name: "package.json" }).first() + await expect(tab).toBeVisible() + await tab.click() + await expect(tab).toHaveAttribute("aria-selected", "true") + + await page.keyboard.press(`${modKey}+W`) + await expect(page.getByRole("tab", { name: "package.json" })).toHaveCount(0) +}) diff --git a/packages/app/e2e/models/model-picker.spec.ts b/packages/app/e2e/models/model-picker.spec.ts index 01e72464c..220a0baa1 100644 --- a/packages/app/e2e/models/model-picker.spec.ts +++ b/packages/app/e2e/models/model-picker.spec.ts @@ -28,7 +28,6 @@ test("smoke model selection updates prompt footer", async ({ page, gotoSession } const key = await target.getAttribute("data-key") if (!key) throw new Error("Failed to resolve model key from list item") - const name = (await target.locator("span").first().innerText()).trim() const model = key.split(":").slice(1).join(":") await input.fill(model) @@ -37,6 +36,13 @@ test("smoke model selection updates prompt footer", async ({ page, gotoSession } await expect(dialog).toHaveCount(0) - const form = page.locator(promptSelector).locator("xpath=ancestor::form[1]") - await expect(form.locator('[data-component="button"]').filter({ hasText: name }).first()).toBeVisible() + await page.locator(promptSelector).click() + await page.keyboard.type("/model") + await expect(command).toBeVisible() + await command.hover() + await page.keyboard.press("Enter") + + const dialogAgain = page.getByRole("dialog") + await expect(dialogAgain).toBeVisible() + await expect(dialogAgain.locator(`[data-slot="list-item"][data-key="${key}"][data-selected="true"]`)).toBeVisible() }) diff --git a/packages/app/e2e/prompt/prompt-drop-file-uri.spec.ts b/packages/app/e2e/prompt/prompt-drop-file-uri.spec.ts new file mode 100644 index 000000000..add2d8d8b --- /dev/null +++ b/packages/app/e2e/prompt/prompt-drop-file-uri.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" + +test("dropping text/plain file: uri inserts a file pill", async ({ page, gotoSession }) => { + await gotoSession() + + const prompt = page.locator(promptSelector) + await prompt.click() + + const path = process.platform === "win32" ? "C:\\opencode-e2e-drop.txt" : "/tmp/opencode-e2e-drop.txt" + const dt = await page.evaluateHandle((text) => { + const dt = new DataTransfer() + dt.setData("text/plain", text) + return dt + }, `file:${path}`) + + await page.dispatchEvent("body", "drop", { dataTransfer: dt }) + + const pill = page.locator(`${promptSelector} [data-type="file"]`).first() + await expect(pill).toBeVisible() + await expect(pill).toHaveAttribute("data-path", path) +}) diff --git a/packages/app/e2e/prompt/prompt-drop-file.spec.ts b/packages/app/e2e/prompt/prompt-drop-file.spec.ts new file mode 100644 index 000000000..0a138de99 --- /dev/null +++ b/packages/app/e2e/prompt/prompt-drop-file.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" + +test("dropping an image file adds an attachment", async ({ page, gotoSession }) => { + await gotoSession() + + const prompt = page.locator(promptSelector) + await prompt.click() + + const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO3+4uQAAAAASUVORK5CYII=" + const dt = await page.evaluateHandle((b64) => { + const dt = new DataTransfer() + const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)) + const file = new File([bytes], "drop.png", { type: "image/png" }) + dt.items.add(file) + return dt + }, png) + + await page.dispatchEvent("body", "drop", { dataTransfer: dt }) + + const img = page.locator('img[alt="drop.png"]').first() + await expect(img).toBeVisible() + + const remove = page.getByRole("button", { name: "Remove attachment" }).first() + await expect(remove).toBeVisible() + + await img.hover() + await remove.click() + await expect(page.locator('img[alt="drop.png"]')).toHaveCount(0) +}) diff --git a/packages/app/e2e/prompt/prompt-multiline.spec.ts b/packages/app/e2e/prompt/prompt-multiline.spec.ts new file mode 100644 index 000000000..216aa3fda --- /dev/null +++ b/packages/app/e2e/prompt/prompt-multiline.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../selectors" + +test("shift+enter inserts a newline without submitting", async ({ page, gotoSession }) => { + await gotoSession() + + await expect(page).toHaveURL(/\/session\/?$/) + + const prompt = page.locator(promptSelector) + await prompt.click() + await page.keyboard.type("line one") + await page.keyboard.press("Shift+Enter") + await page.keyboard.type("line two") + + await expect(page).toHaveURL(/\/session\/?$/) + await expect(prompt).toContainText("line one") + await expect(prompt).toContainText("line two") +}) diff --git a/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts b/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts new file mode 100644 index 000000000..eefce19dc --- /dev/null +++ b/packages/app/e2e/prompt/prompt-slash-terminal.spec.ts @@ -0,0 +1,23 @@ +import { test, expect } from "../fixtures" +import { promptSelector, terminalSelector } from "../selectors" + +test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => { + await gotoSession() + + const prompt = page.locator(promptSelector) + const terminal = page.locator(terminalSelector) + + await expect(terminal).not.toBeVisible() + + await prompt.click() + await page.keyboard.type("/terminal") + await expect(page.locator('[data-slash-id="terminal.toggle"]').first()).toBeVisible() + await page.keyboard.press("Enter") + await expect(terminal).toBeVisible() + + await prompt.click() + await page.keyboard.type("/terminal") + await expect(page.locator('[data-slash-id="terminal.toggle"]').first()).toBeVisible() + await page.keyboard.press("Enter") + await expect(terminal).not.toBeVisible() +}) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 1121c2e95..1be9f38d7 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -1,35 +1,36 @@ import "@/index.css" -import { ErrorBoundary, Show, Suspense, lazy, type JSX, type ParentProps } from "solid-js" -import { Router, Route, Navigate } from "@solidjs/router" -import { MetaProvider } from "@solidjs/meta" -import { Font } from "@opencode-ai/ui/font" -import { MarkedProvider } from "@opencode-ai/ui/context/marked" -import { DiffComponentProvider } from "@opencode-ai/ui/context/diff" -import { CodeComponentProvider } from "@opencode-ai/ui/context/code" -import { I18nProvider } from "@opencode-ai/ui/context" -import { Diff } from "@opencode-ai/ui/diff" import { Code } from "@opencode-ai/ui/code" +import { I18nProvider } from "@opencode-ai/ui/context" +import { CodeComponentProvider } from "@opencode-ai/ui/context/code" +import { DialogProvider } from "@opencode-ai/ui/context/dialog" +import { DiffComponentProvider } from "@opencode-ai/ui/context/diff" +import { MarkedProvider } from "@opencode-ai/ui/context/marked" +import { Diff } from "@opencode-ai/ui/diff" +import { Font } from "@opencode-ai/ui/font" import { ThemeProvider } from "@opencode-ai/ui/theme" -import { GlobalSyncProvider } from "@/context/global-sync" -import { PermissionProvider } from "@/context/permission" -import { LayoutProvider } from "@/context/layout" +import { MetaProvider } from "@solidjs/meta" +import { Navigate, Route, Router } from "@solidjs/router" +import { ErrorBoundary, type JSX, lazy, type ParentProps, Show, Suspense } from "solid-js" +import { CommandProvider } from "@/context/command" +import { CommentsProvider } from "@/context/comments" +import { FileProvider } from "@/context/file" import { GlobalSDKProvider } from "@/context/global-sdk" -import { normalizeServerUrl, ServerProvider, useServer } from "@/context/server" +import { GlobalSyncProvider } from "@/context/global-sync" +import { HighlightsProvider } from "@/context/highlights" +import { LanguageProvider, useLanguage } from "@/context/language" +import { LayoutProvider } from "@/context/layout" +import { ModelsProvider } from "@/context/models" +import { NotificationProvider } from "@/context/notification" +import { PermissionProvider } from "@/context/permission" +import { usePlatform } from "@/context/platform" +import { PromptProvider } from "@/context/prompt" +import { type ServerConnection, ServerProvider, useServer } from "@/context/server" import { SettingsProvider } from "@/context/settings" import { TerminalProvider } from "@/context/terminal" -import { PromptProvider } from "@/context/prompt" -import { FileProvider } from "@/context/file" -import { CommentsProvider } from "@/context/comments" -import { NotificationProvider } from "@/context/notification" -import { ModelsProvider } from "@/context/models" -import { DialogProvider } from "@opencode-ai/ui/context/dialog" -import { CommandProvider } from "@/context/command" -import { LanguageProvider, useLanguage } from "@/context/language" -import { usePlatform } from "@/context/platform" -import { HighlightsProvider } from "@/context/highlights" -import Layout from "@/pages/layout" import DirectoryLayout from "@/pages/directory-layout" +import Layout from "@/pages/layout" import { ErrorPage } from "./pages/error" + const Home = lazy(() => import("@/pages/home")) const Session = lazy(() => import("@/pages/session")) const Loading = () =>
@@ -57,7 +58,11 @@ function UiI18nBridge(props: ParentProps) { declare global { interface Window { - __OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[]; wsl?: boolean } + __OPENCODE__?: { + updaterEnabled?: boolean + deepLinks?: string[] + wsl?: boolean + } } } @@ -107,30 +112,6 @@ function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) { ) } -const getStoredDefaultServerUrl = (platform: ReturnType) => { - if (platform.platform !== "web") return - const result = platform.getDefaultServerUrl?.() - if (result instanceof Promise) return - if (!result) return - return normalizeServerUrl(result) -} - -const resolveDefaultServerUrl = (props: { - defaultUrl?: string - storedDefaultServerUrl?: string - hostname: string - origin: string - isDev: boolean - devHost?: string - devPort?: string -}) => { - if (props.defaultUrl) return props.defaultUrl - if (props.storedDefaultServerUrl) return props.storedDefaultServerUrl - if (props.hostname.includes("opencode.ai")) return "http://localhost:4096" - if (props.isDev) return `http://${props.devHost ?? "localhost"}:${props.devPort ?? "4096"}` - return props.origin -} - export function AppBaseProviders(props: ParentProps) { return ( @@ -157,27 +138,19 @@ export function AppBaseProviders(props: ParentProps) { function ServerKey(props: ParentProps) { const server = useServer() return ( - + {props.children} ) } -export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element; isSidecar?: boolean }) { - const platform = usePlatform() - const storedDefaultServerUrl = getStoredDefaultServerUrl(platform) - const defaultServerUrl = resolveDefaultServerUrl({ - defaultUrl: props.defaultUrl, - storedDefaultServerUrl, - hostname: location.hostname, - origin: window.location.origin, - isDev: import.meta.env.DEV, - devHost: import.meta.env.VITE_OPENCODE_SERVER_HOST, - devPort: import.meta.env.VITE_OPENCODE_SERVER_PORT, - }) - +export function AppInterface(props: { + children?: JSX.Element + defaultServer: ServerConnection.Key + servers?: Array +}) { return ( - + diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index a196db231..9f7afb8cd 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -121,7 +121,7 @@ export function ModelSelectorPopover(props: { }} modal={false} placement="top-start" - gutter={8} + gutter={4} > {props.children} diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 4c3780636..fa5d2d36c 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -1,19 +1,18 @@ -import { createResource, createEffect, createMemo, onCleanup, Show } from "solid-js" -import { createStore, reconcile } from "solid-js/store" +import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" -import { List } from "@opencode-ai/ui/list" -import { Button } from "@opencode-ai/ui/button" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { TextField } from "@opencode-ai/ui/text-field" -import { normalizeServerUrl, useServer } from "@/context/server" -import { usePlatform } from "@/context/platform" -import { useNavigate } from "@solidjs/router" -import { useLanguage } from "@/context/language" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" -import { useGlobalSDK } from "@/context/global-sdk" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { List } from "@opencode-ai/ui/list" +import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@opencode-ai/ui/toast" +import { useNavigate } from "@solidjs/router" +import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js" +import { createStore, reconcile } from "solid-js/store" import { ServerRow } from "@/components/server/server-row" +import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" +import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" import { checkServerHealth, type ServerHealth } from "@/utils/server-health" interface AddRowProps { @@ -89,7 +88,7 @@ function useServerPreview(fetcher: typeof fetch) { if (!looksComplete(value)) return const normalized = normalizeServerUrl(value) if (!normalized) return - const result = await checkServerHealth(normalized, fetcher) + const result = await checkServerHealth({ url: normalized }, fetcher) setStatus(result.healthy) } @@ -171,14 +170,13 @@ export function DialogSelectServer() { const dialog = useDialog() const server = useServer() const platform = usePlatform() - const globalSDK = useGlobalSDK() const language = useLanguage() const fetcher = platform.fetch ?? globalThis.fetch const { defaultUrl, canDefault, setDefault } = useDefaultServer(platform, language) const { previewStatus } = useServerPreview(fetcher) let listRoot: HTMLDivElement | undefined const [store, setStore] = createStore({ - status: {} as Record, + status: {} as Record, addServer: { url: "", adding: false, @@ -214,24 +212,25 @@ export function DialogSelectServer() { }) } - const replaceServer = (original: string, next: string) => { - const active = server.url - const nextActive = active === original ? next : active + const replaceServer = (original: ServerConnection.Http, next: string) => { + const active = server.key + const newConn = server.add(next) + if (!newConn) return - server.add(next) + const nextActive = active === ServerConnection.key(original) ? ServerConnection.key(newConn) : active if (nextActive) server.setActive(nextActive) - server.remove(original) + server.remove(ServerConnection.key(original)) } const items = createMemo(() => { - const current = server.url + const current = server.current const list = server.list if (!current) return list if (!list.includes(current)) return [current, ...list] return [current, ...list.filter((x) => x !== current)] }) - const current = createMemo(() => items().find((x) => x === server.url) ?? items()[0]) + const current = createMemo(() => items().find((x) => ServerConnection.key(x) === server.key) ?? items()[0]) const sortedItems = createMemo(() => { const list = items() @@ -246,17 +245,17 @@ export function DialogSelectServer() { return list.slice().sort((a, b) => { if (a === active) return -1 if (b === active) return 1 - const diff = rank(store.status[a]) - rank(store.status[b]) + const diff = rank(store.status[ServerConnection.key(a)]) - rank(store.status[ServerConnection.key(b)]) if (diff !== 0) return diff return (order.get(a) ?? 0) - (order.get(b) ?? 0) }) }) async function refreshHealth() { - const results: Record = {} + const results: Record = {} await Promise.all( - items().map(async (url) => { - results[url] = await checkServerHealth(url, fetcher) + items().map(async (conn) => { + results[ServerConnection.key(conn)] = await checkServerHealth(conn.http, fetcher) }), ) setStore("status", reconcile(results)) @@ -269,15 +268,15 @@ export function DialogSelectServer() { onCleanup(() => clearInterval(interval)) }) - async function select(value: string, persist?: boolean) { - if (!persist && store.status[value]?.healthy === false) return + async function select(conn: ServerConnection.Any, persist?: boolean) { + if (!persist && store.status[ServerConnection.key(conn)]?.healthy === false) return dialog.close() if (persist) { - server.add(value) + server.add(conn.http.url) navigate("/") return } - server.setActive(value) + server.setActive(ServerConnection.key(conn)) navigate("/") } @@ -311,7 +310,7 @@ export function DialogSelectServer() { setStore("addServer", { adding: true, error: "" }) - const result = await checkServerHealth(normalized, fetcher) + const result = await checkServerHealth({ url: normalized }, fetcher) setStore("addServer", { adding: false }) if (!result.healthy) { @@ -320,25 +319,25 @@ export function DialogSelectServer() { } resetAdd() - await select(normalized, true) + await select({ type: "http", http: { url: normalized } }, true) } - async function handleEdit(original: string, value: string) { - if (store.editServer.busy) return + async function handleEdit(original: ServerConnection.Any, value: string) { + if (store.editServer.busy || original.type !== "http") return const normalized = normalizeServerUrl(value) if (!normalized) { resetEdit() return } - if (normalized === original) { + if (normalized === original.http.url) { resetEdit() return } setStore("editServer", { busy: true, error: "" }) - const result = await checkServerHealth(normalized, fetcher) + const result = await checkServerHealth({ url: normalized }, fetcher) setStore("editServer", { busy: false }) if (!result.healthy) { @@ -366,7 +365,7 @@ export function DialogSelectServer() { handleAdd(store.addServer.url) } - const handleEditKey = (event: KeyboardEvent, original: string) => { + const handleEditKey = (event: KeyboardEvent, original: ServerConnection.Any) => { event.stopPropagation() if (event.key === "Escape") { event.preventDefault() @@ -378,7 +377,7 @@ export function DialogSelectServer() { handleEdit(original, store.editServer.value) } - async function handleRemove(url: string) { + async function handleRemove(url: ServerConnection.Key) { server.remove(url) if ((await platform.getDefaultServerUrl?.()) === url) { platform.setDefaultServerUrl?.(null) @@ -390,11 +389,14 @@ export function DialogSelectServer() {
(listRoot = el)}> x} + key={(x) => x.http.url} onSelect={(x) => { if (x) select(x) }} @@ -428,7 +430,7 @@ export function DialogSelectServer() { return (
+ {language.t("dialog.server.status.default")} @@ -456,59 +458,63 @@ export function DialogSelectServer() { } /> - +

{language.t("dialog.server.current")}

- - e.stopPropagation()} - onPointerDown={(e: PointerEvent) => e.stopPropagation()} - /> - - - { - setStore("editServer", { - id: i, - value: i, - error: "", - status: store.status[i]?.healthy, - }) - }} - > - {language.t("dialog.server.menu.edit")} - - - setDefault(i)}> + + + e.stopPropagation()} + onPointerDown={(e: PointerEvent) => e.stopPropagation()} + /> + + + { + setStore("editServer", { + id: i.http.url, + value: i.http.url, + error: "", + status: store.status[ServerConnection.key(i)]?.healthy, + }) + }} + > + {language.t("dialog.server.menu.edit")} + + + setDefault(i.http.url)}> + + {language.t("dialog.server.menu.default")} + + + + + setDefault(null)}> + + {language.t("dialog.server.menu.defaultRemove")} + + + + + handleRemove(ServerConnection.key(i))} + class="text-text-on-critical-base hover:bg-surface-critical-weak" + > - {language.t("dialog.server.menu.default")} + {language.t("dialog.server.menu.delete")} - - - setDefault(null)}> - - {language.t("dialog.server.menu.defaultRemove")} - - - - - handleRemove(i)} - class="text-text-on-critical-base hover:bg-surface-critical-weak" - > - {language.t("dialog.server.menu.delete")} - - - - + + + +
diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index 758f5a83f..cec094354 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -71,13 +71,13 @@ const kindLabel = (kind: Kind) => { const kindTextColor = (kind: Kind) => { if (kind === "add") return "color: var(--icon-diff-add-base)" if (kind === "del") return "color: var(--icon-diff-delete-base)" - return "color: var(--icon-warning-active)" + return "color: var(--icon-diff-modified-base)" } const kindDotColor = (kind: Kind) => { if (kind === "add") return "background-color: var(--icon-diff-add-base)" if (kind === "del") return "background-color: var(--icon-diff-delete-base)" - return "background-color: var(--icon-warning-active)" + return "background-color: var(--icon-diff-modified-base)" } const visibleKind = (node: FileNode, kinds?: ReadonlyMap, marks?: Set) => { @@ -447,12 +447,13 @@ export default function FileTree(props: { }) return ( -
+
{(node) => { const expanded = () => file.tree.state(node.path)?.expanded ?? false const deep = () => deeps().get(node.path) ?? -1 const kind = () => visibleKind(node, kinds(), marks()) + const active = () => !!kind() && !node.ignored return ( @@ -530,7 +531,37 @@ export default function FileTree(props: { onClick={() => props.onFileClick?.(node)} >
- + + + + + + + + + + + + + + diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 984888c35..599711a9b 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1,5 +1,5 @@ import { useFilteredList } from "@opencode-ai/ui/hooks" -import { createEffect, on, Component, Show, For, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js" +import { createEffect, on, Component, Show, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js" import { createStore } from "solid-js/store" import { createFocusSignal } from "@solid-primitives/active-element" import { useLocal } from "@/context/local" @@ -26,13 +26,13 @@ import type { IconName } from "@opencode-ai/ui/icons/provider" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { IconButton } from "@opencode-ai/ui/icon-button" import { Select } from "@opencode-ai/ui/select" +import { RadioGroup } from "@opencode-ai/ui/radio-group" import { useDialog } from "@opencode-ai/ui/context/dialog" import { ModelSelectorPopover } from "@/components/dialog-select-model" import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid" import { useProviders } from "@/hooks/use-providers" import { useCommand } from "@/context/command" import { Persist, persisted } from "@/utils/persist" -import { SessionContextUsage } from "@/components/session-context-usage" import { usePermission } from "@/context/permission" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" @@ -94,7 +94,6 @@ export const PromptInput: Component = (props) => { const local = useLocal() const files = useFile() const prompt = usePrompt() - const commentCount = createMemo(() => prompt.context.items().filter((item) => !!item.comment?.trim()).length) const layout = useLayout() const comments = useComments() const params = useParams() @@ -105,11 +104,12 @@ export const PromptInput: Component = (props) => { const language = useLanguage() const platform = usePlatform() let editorRef!: HTMLDivElement - let fileInputRef!: HTMLInputElement + let fileInputRef: HTMLInputElement | undefined let scrollRef!: HTMLDivElement let slashPopoverRef!: HTMLDivElement const mirror = { input: false } + const inset = 44 const scrollCursorIntoView = () => { const container = scrollRef @@ -119,7 +119,14 @@ export const PromptInput: Component = (props) => { const range = selection.getRangeAt(0) if (!editorRef.contains(range.startContainer)) return - const rect = range.getBoundingClientRect() + const cursor = getCursorPosition(editorRef) + const length = promptLength(prompt.current().filter((part) => part.type !== "image")) + if (cursor >= length) { + container.scrollTop = container.scrollHeight + return + } + + const rect = range.getClientRects().item(0) ?? range.getBoundingClientRect() if (!rect.height) return const containerRect = container.getBoundingClientRect() @@ -132,8 +139,8 @@ export const PromptInput: Component = (props) => { return } - if (bottom > container.scrollTop + container.clientHeight - padding) { - container.scrollTop = bottom - container.clientHeight + padding + if (bottom > container.scrollTop + container.clientHeight - inset) { + container.scrollTop = bottom - container.clientHeight + inset } } @@ -223,16 +230,26 @@ export const PromptInput: Component = (props) => { mode: "normal", applyingHistory: false, }) - const placeholder = createMemo(() => - promptPlaceholder({ - mode: store.mode, - commentCount: commentCount(), - example: language.t(EXAMPLES[store.placeholder]), - t: (key, params) => language.t(key as Parameters[0], params as never), - }), - ) - const MAX_HISTORY = 100 + const commentCount = createMemo(() => { + if (store.mode === "shell") return 0 + return prompt.context.items().filter((item) => !!item.comment?.trim()).length + }) + + const contextItems = createMemo(() => { + const items = prompt.context.items() + if (store.mode !== "shell") return items + return items.filter((item) => !item.comment?.trim()) + }) + + const hasUserPrompt = createMemo(() => { + const sessionID = params.id + if (!sessionID) return false + const messages = sync.data.message[sessionID] + if (!messages) return false + return messages.some((m) => m.role === "user") + }) + const [history, setHistory] = persisted( Persist.global("prompt-history", ["prompt-history.v1"]), createStore<{ @@ -250,6 +267,18 @@ export const PromptInput: Component = (props) => { }), ) + const suggest = createMemo(() => !hasUserPrompt()) + + const placeholder = createMemo(() => + promptPlaceholder({ + mode: store.mode, + commentCount: commentCount(), + example: suggest() ? language.t(EXAMPLES[store.placeholder]) : "", + suggest: suggest(), + t: (key, params) => language.t(key as Parameters[0], params as never), + }), + ) + const applyHistoryPrompt = (p: Prompt, position: "start" | "end") => { const length = position === "start" ? 0 : promptLength(p) setStore("applyingHistory", true) @@ -280,6 +309,45 @@ export const PromptInput: Component = (props) => { } const isFocused = createFocusSignal(() => editorRef) + const escBlur = () => platform.platform === "desktop" && platform.os === "macos" + + const pick = () => fileInputRef?.click() + + const setMode = (mode: "normal" | "shell") => { + setStore("mode", mode) + setStore("popover", null) + requestAnimationFrame(() => editorRef?.focus()) + } + + const shellModeKey = "mod+shift+x" + const normalModeKey = "mod+shift+e" + + command.register("prompt-input", () => [ + { + id: "file.attach", + title: language.t("prompt.action.attachFile"), + category: language.t("command.category.file"), + keybind: "mod+u", + disabled: store.mode !== "normal", + onSelect: pick, + }, + { + id: "prompt.mode.shell", + title: language.t("command.prompt.mode.shell"), + category: language.t("command.category.session"), + keybind: shellModeKey, + disabled: store.mode === "shell", + onSelect: () => setMode("shell"), + }, + { + id: "prompt.mode.normal", + title: language.t("command.prompt.mode.normal"), + category: language.t("command.category.session"), + keybind: normalModeKey, + disabled: store.mode === "normal", + onSelect: () => setMode("normal"), + }, + ]) const closePopover = () => setStore("popover", null) @@ -325,6 +393,7 @@ export const PromptInput: Component = (props) => { createEffect(() => { params.id if (params.id) return + if (!suggest()) return const interval = setInterval(() => { setStore("placeholder", (prev) => (prev + 1) % EXAMPLES.length) }, 6500) @@ -334,15 +403,10 @@ export const PromptInput: Component = (props) => { const [composing, setComposing] = createSignal(false) const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229 - createEffect(() => { - if (!isFocused()) closePopover() - }) - - // Safety: reset composing state on focus change to prevent stuck state - // This handles edge cases where compositionend event may not fire - createEffect(() => { - if (!isFocused()) setComposing(false) - }) + const handleBlur = () => { + closePopover() + setComposing(false) + } const agentList = createMemo(() => sync.data.agent @@ -815,6 +879,13 @@ export const PromptInput: Component = (props) => { }) const handleKeyDown = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "u") { + event.preventDefault() + if (store.mode !== "normal") return + pick() + return + } + if (event.key === "Backspace") { const selection = window.getSelection() if (selection && selection.isCollapsed) { @@ -842,13 +913,39 @@ export const PromptInput: Component = (props) => { return } } - if (store.mode === "shell") { - const { collapsed, cursorPosition, textLength } = getCaretState() - if (event.key === "Escape") { - setStore("mode", "normal") + + if (event.key === "Escape") { + if (store.popover) { + closePopover() event.preventDefault() + event.stopPropagation() return } + + if (store.mode === "shell") { + setStore("mode", "normal") + event.preventDefault() + event.stopPropagation() + return + } + + if (working()) { + abort() + event.preventDefault() + event.stopPropagation() + return + } + + if (escBlur()) { + editorRef.blur() + event.preventDefault() + event.stopPropagation() + return + } + } + + if (store.mode === "shell") { + const { collapsed, cursorPosition, textLength } = getCaretState() if (event.key === "Backspace" && collapsed && cursorPosition === 0 && textLength === 0) { setStore("mode", "normal") event.preventDefault() @@ -927,17 +1024,12 @@ export const PromptInput: Component = (props) => { if (event.key === "Enter" && !event.shiftKey) { handleSubmit(event) } - if (event.key === "Escape") { - if (store.popover) { - closePopover() - } else if (working()) { - abort() - } - } } + const variants = createMemo(() => ["default", ...local.model.variant.list()]) + return ( -
+
(slashPopoverRef = el)} @@ -957,8 +1049,8 @@ export const PromptInput: Component = (props) => { onSubmit={handleSubmit} classList={{ "group/prompt-input": true, - "bg-surface-raised-stronger-non-alpha shadow-xs-border relative": true, - "rounded-[14px] overflow-clip focus-within:shadow-xs-border": true, + "bg-surface-raised-stronger-non-alpha shadow-xs-border relative z-10": true, + "rounded-[12px] overflow-clip focus-within:shadow-xs-border": true, "border-icon-info-active border-dashed": store.draggingType !== null, [props.class ?? ""]: !!props.class, }} @@ -968,7 +1060,7 @@ export const PromptInput: Component = (props) => { label={language.t(store.draggingType === "@mention" ? "prompt.dropzone.file.label" : "prompt.dropzone.label")} /> { const active = comments.active() return !!item.commentID && item.commentID === active?.id && item.path === active?.file @@ -988,162 +1080,60 @@ export const PromptInput: Component = (props) => { onRemove={removeImageAttachment} removeLabel={language.t("prompt.attachment.remove")} /> -
(scrollRef = el)}> -
{ - editorRef = el - props.ref?.(el) - }} - role="textbox" - aria-multiline="true" - aria-label={placeholder()} - contenteditable="true" - autocapitalize="off" - autocorrect="off" - spellcheck={false} - onInput={handleInput} - onPaste={handlePaste} - onCompositionStart={() => setComposing(true)} - onCompositionEnd={() => setComposing(false)} - onKeyDown={handleKeyDown} - classList={{ - "select-text": true, - "w-full p-3 pr-12 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, - "[&_[data-type=file]]:text-syntax-property": true, - "[&_[data-type=agent]]:text-syntax-type": true, - "font-mono!": store.mode === "shell", - }} - /> - -
- {placeholder()} -
-
-
-
-
- - -
- - {language.t("prompt.mode.shell")} - {language.t("prompt.mode.shell.exit")} -
-
- - - = (props) => { e.currentTarget.value = "" }} /> -
- - - - - - -
- - -
- {language.t("prompt.action.stop")} - {language.t("common.key.esc")} -
-
- -
- {language.t("prompt.action.send")} - -
-
-
- } + +
- - + + + + + + +
+ {language.t("prompt.action.stop")} + {language.t("common.key.esc")} +
+
+ +
+ {language.t("prompt.action.send")} + +
+
+ + } + > + +
+
+ + +
+
+ + + +
+
+
+ +
+
+
+ +
+ {language.t("prompt.mode.shell")} +
+
+ + + + + (x === "default" ? language.t("common.default") : x)} + onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)} + class="capitalize max-w-[160px]" + valueClass="truncate text-13-regular" + triggerStyle={{ height: "28px" }} + variant="ghost" + /> + + +
+
+ mode} + label={(mode) => ( + + + + )} + onSelect={(mode) => mode && setMode(mode)} + fill + pad="none" + class="w-[68px]" + /> +
+
+
+
) } diff --git a/packages/app/src/components/prompt-input/context-items.tsx b/packages/app/src/components/prompt-input/context-items.tsx index b575c3961..b138fe3ef 100644 --- a/packages/app/src/components/prompt-input/context-items.tsx +++ b/packages/app/src/components/prompt-input/context-items.tsx @@ -41,10 +41,9 @@ export const PromptContextItems: Component = (props) => { >
props.openComment(item)} diff --git a/packages/app/src/components/prompt-input/placeholder.test.ts b/packages/app/src/components/prompt-input/placeholder.test.ts index b633df829..5f6aa59e9 100644 --- a/packages/app/src/components/prompt-input/placeholder.test.ts +++ b/packages/app/src/components/prompt-input/placeholder.test.ts @@ -9,27 +9,40 @@ describe("promptPlaceholder", () => { mode: "shell", commentCount: 0, example: "example", + suggest: true, t, }) expect(value).toBe("prompt.placeholder.shell") }) test("returns summarize placeholders for comment context", () => { - expect(promptPlaceholder({ mode: "normal", commentCount: 1, example: "example", t })).toBe( + expect(promptPlaceholder({ mode: "normal", commentCount: 1, example: "example", suggest: true, t })).toBe( "prompt.placeholder.summarizeComment", ) - expect(promptPlaceholder({ mode: "normal", commentCount: 2, example: "example", t })).toBe( + expect(promptPlaceholder({ mode: "normal", commentCount: 2, example: "example", suggest: true, t })).toBe( "prompt.placeholder.summarizeComments", ) }) - test("returns default placeholder with example", () => { + test("returns default placeholder with example when suggestions enabled", () => { const value = promptPlaceholder({ mode: "normal", commentCount: 0, example: "translated-example", + suggest: true, t, }) expect(value).toBe("prompt.placeholder.normal:translated-example") }) + + test("returns simple placeholder when suggestions disabled", () => { + const value = promptPlaceholder({ + mode: "normal", + commentCount: 0, + example: "translated-example", + suggest: false, + t, + }) + expect(value).toBe("prompt.placeholder.simple") + }) }) diff --git a/packages/app/src/components/prompt-input/placeholder.ts b/packages/app/src/components/prompt-input/placeholder.ts index 07f6a43b5..395fee51b 100644 --- a/packages/app/src/components/prompt-input/placeholder.ts +++ b/packages/app/src/components/prompt-input/placeholder.ts @@ -2,6 +2,7 @@ type PromptPlaceholderInput = { mode: "normal" | "shell" commentCount: number example: string + suggest: boolean t: (key: string, params?: Record) => string } @@ -9,5 +10,6 @@ export function promptPlaceholder(input: PromptPlaceholderInput) { if (input.mode === "shell") return input.t("prompt.placeholder.shell") if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments") if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment") + if (!input.suggest) return input.t("prompt.placeholder.simple") return input.t("prompt.placeholder.normal", { example: input.example }) } diff --git a/packages/app/src/components/prompt-input/slash-popover.tsx b/packages/app/src/components/prompt-input/slash-popover.tsx index 259883d61..65eb01c79 100644 --- a/packages/app/src/components/prompt-input/slash-popover.tsx +++ b/packages/app/src/components/prompt-input/slash-popover.tsx @@ -40,9 +40,9 @@ export const PromptPopover: Component = (props) => { ref={(el) => { if (props.popover === "slash") props.setSlashPopoverRef(el) }} - class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-80 min-h-10 - overflow-auto no-scrollbar flex flex-col p-2 rounded-md - border border-border-base bg-surface-raised-stronger-non-alpha shadow-md" + class="absolute inset-x-0 -top-2 -translate-y-full origin-bottom-left max-h-80 min-h-10 + overflow-auto no-scrollbar flex flex-col p-2 rounded-[12px] + bg-surface-raised-stronger-non-alpha shadow-[var(--shadow-lg-border-base)]" onMouseDown={(e) => e.preventDefault()} > diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index 475a0e20f..c3d6a9281 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -12,24 +12,27 @@ let selected = "/repo/worktree-a" const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }] -const clientFor = (directory: string) => ({ - session: { - create: async () => { - createdSessions.push(directory) - return { data: { id: `session-${createdSessions.length}` } } +const clientFor = (directory: string) => { + createdClients.push(directory) + return { + session: { + create: async () => { + createdSessions.push(directory) + return { data: { id: `session-${createdSessions.length}` } } + }, + shell: async () => { + sentShell.push(directory) + return { data: undefined } + }, + prompt: async () => ({ data: undefined }), + command: async () => ({ data: undefined }), + abort: async () => ({ data: undefined }), }, - shell: async () => { - sentShell.push(directory) - return { data: undefined } + worktree: { + create: async () => ({ data: { directory: `${directory}/new` } }), }, - prompt: async () => ({ data: undefined }), - command: async () => ({ data: undefined }), - abort: async () => ({ data: undefined }), - }, - worktree: { - create: async () => ({ data: { directory: `${directory}/new` } }), - }, -}) + } +} beforeAll(async () => { const rootClient = clientFor("/repo/main") @@ -88,11 +91,17 @@ beforeAll(async () => { })) mock.module("@/context/sdk", () => ({ - useSDK: () => ({ - directory: "/repo/main", - client: rootClient, - url: "http://localhost:4096", - }), + useSDK: () => { + const sdk = { + directory: "/repo/main", + client: rootClient, + url: "http://localhost:4096", + createClient(opts: any) { + return clientFor(opts.directory) + }, + } + return sdk + }, })) mock.module("@/context/sync", () => ({ diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 9a1fba5d5..8a3dfc40d 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -1,21 +1,20 @@ -import { Accessor } from "solid-js" -import { useNavigate, useParams } from "@solidjs/router" -import { createOpencodeClient, type Message } from "@opencode-ai/sdk/v2/client" +import type { Message } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" import { base64Encode } from "@opencode-ai/util/encode" -import { useLocal } from "@/context/local" -import { usePrompt, type ImageAttachmentPart, type Prompt } from "@/context/prompt" +import { useNavigate, useParams } from "@solidjs/router" +import type { Accessor } from "solid-js" +import type { FileSelection } from "@/context/file" +import { useGlobalSync } from "@/context/global-sync" +import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" +import { useLocal } from "@/context/local" +import { type ImageAttachmentPart, type Prompt, usePrompt } from "@/context/prompt" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" -import { useGlobalSync } from "@/context/global-sync" -import { usePlatform } from "@/context/platform" -import { useLanguage } from "@/context/language" import { Identifier } from "@/utils/id" import { Worktree as WorktreeState } from "@/utils/worktree" -import type { FileSelection } from "@/context/file" -import { setCursorPosition } from "./editor-dom" import { buildRequestParts } from "./build-request-parts" +import { setCursorPosition } from "./editor-dom" type PendingPrompt = { abort: AbortController @@ -56,7 +55,6 @@ export function createPromptSubmit(input: PromptSubmitInput) { const sdk = useSDK() const sync = useSync() const globalSync = useGlobalSync() - const platform = usePlatform() const local = useLocal() const prompt = usePrompt() const layout = useLayout() @@ -80,6 +78,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { queued.abort.abort() queued.cleanup() pending.delete(sessionID) + globalSync.todo.set(sessionID, undefined) return Promise.resolve() } return sdk.client.session @@ -87,6 +86,9 @@ export function createPromptSubmit(input: PromptSubmitInput) { sessionID, }) .catch(() => {}) + .finally(() => { + globalSync.todo.set(sessionID, undefined) + }) } const restoreCommentItems = (items: CommentItem[]) => { @@ -171,9 +173,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { } if (sessionDirectory !== projectDirectory) { - client = createOpencodeClient({ - baseUrl: sdk.url, - fetch: platform.fetch, + client = sdk.createClient({ directory: sessionDirectory, throwOnError: true, }) @@ -368,7 +368,10 @@ export function createPromptSubmit(input: PromptSubmitInput) { const timer = { id: undefined as number | undefined } const timeout = new Promise>>((resolve) => { timer.id = window.setTimeout(() => { - resolve({ status: "failed", message: language.t("workspace.error.stillPreparing") }) + resolve({ + status: "failed", + message: language.t("workspace.error.stillPreparing"), + }) }, timeoutMs) }) diff --git a/packages/app/src/components/question-dock.tsx b/packages/app/src/components/question-dock.tsx index 5054253b8..cd2e495b1 100644 --- a/packages/app/src/components/question-dock.tsx +++ b/packages/app/src/components/question-dock.tsx @@ -1,6 +1,7 @@ -import { For, Show, createMemo, type Component } from "solid-js" +import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js" import { createStore } from "solid-js/store" import { Button } from "@opencode-ai/ui/button" +import { DockPrompt } from "@opencode-ai/ui/dock-prompt" import { Icon } from "@opencode-ai/ui/icon" import { showToast } from "@opencode-ai/ui/toast" import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2" @@ -12,25 +13,98 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => const language = useLanguage() const questions = createMemo(() => props.request.questions) - const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true) + const total = createMemo(() => questions().length) const [store, setStore] = createStore({ tab: 0, answers: [] as QuestionAnswer[], custom: [] as string[], + customOn: [] as boolean[], editing: false, sending: false, }) + let root: HTMLDivElement | undefined + const question = createMemo(() => questions()[store.tab]) - const confirm = createMemo(() => !single() && store.tab === questions().length) const options = createMemo(() => question()?.options ?? []) const input = createMemo(() => store.custom[store.tab] ?? "") + const on = createMemo(() => store.customOn[store.tab] === true) const multi = createMemo(() => question()?.multiple === true) - const customPicked = createMemo(() => { - const value = input() - if (!value) return false - return store.answers[store.tab]?.includes(value) ?? false + + const summary = createMemo(() => { + const n = Math.min(store.tab + 1, total()) + return `${n} of ${total()} questions` + }) + + const last = createMemo(() => store.tab >= total() - 1) + + const customUpdate = (value: string, selected: boolean = on()) => { + const prev = input().trim() + const next = value.trim() + + setStore("custom", store.tab, value) + if (!selected) return + + if (multi()) { + setStore("answers", store.tab, (current = []) => { + const removed = prev ? current.filter((item) => item.trim() !== prev) : current + if (!next) return removed + if (removed.some((item) => item.trim() === next)) return removed + return [...removed, next] + }) + return + } + + setStore("answers", store.tab, next ? [next] : []) + } + + const measure = () => { + if (!root) return + + const scroller = document.querySelector(".session-scroller") + const head = scroller instanceof HTMLElement ? scroller.firstElementChild : undefined + const top = + head instanceof HTMLElement && head.classList.contains("sticky") ? head.getBoundingClientRect().bottom : 0 + if (!top) { + root.style.removeProperty("--question-prompt-max-height") + return + } + + const dock = root.closest('[data-component="session-prompt-dock"]') + if (!(dock instanceof HTMLElement)) return + + const dockBottom = dock.getBoundingClientRect().bottom + const below = Math.max(0, dockBottom - root.getBoundingClientRect().bottom) + const gap = 8 + const max = Math.max(240, Math.floor(dockBottom - top - gap - below)) + root.style.setProperty("--question-prompt-max-height", `${max}px`) + } + + onMount(() => { + let raf: number | undefined + const update = () => { + if (raf !== undefined) cancelAnimationFrame(raf) + raf = requestAnimationFrame(() => { + raf = undefined + measure() + }) + } + + update() + window.addEventListener("resize", update) + + const dock = root?.closest('[data-component="session-prompt-dock"]') + const scroller = document.querySelector(".session-scroller") + const observer = new ResizeObserver(update) + if (dock instanceof HTMLElement) observer.observe(dock) + if (scroller instanceof HTMLElement) observer.observe(scroller) + + onCleanup(() => { + window.removeEventListener("resize", update) + observer.disconnect() + if (raf !== undefined) cancelAnimationFrame(raf) + }) }) const fail = (err: unknown) => { @@ -64,23 +138,13 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => } } - const submit = () => { - void reply(questions().map((_, i) => store.answers[i] ?? [])) - } + const submit = () => void reply(questions().map((_, i) => store.answers[i] ?? [])) const pick = (answer: string, custom: boolean = false) => { setStore("answers", store.tab, [answer]) - - if (custom) { - setStore("custom", store.tab, answer) - } - - if (single()) { - void reply([[answer]]) - return - } - - setStore("tab", store.tab + 1) + if (custom) setStore("custom", store.tab, answer) + if (!custom) setStore("customOn", store.tab, false) + setStore("editing", false) } const toggle = (answer: string) => { @@ -90,16 +154,41 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => }) } - const selectTab = (index: number) => { - setStore("tab", index) + const customToggle = () => { + if (store.sending) return + + if (!multi()) { + setStore("customOn", store.tab, true) + setStore("editing", true) + customUpdate(input(), true) + return + } + + const next = !on() + setStore("customOn", store.tab, next) + if (next) { + setStore("editing", true) + customUpdate(input(), true) + return + } + + const value = input().trim() + if (value) setStore("answers", store.tab, (current = []) => current.filter((item) => item.trim() !== value)) setStore("editing", false) } + const customOpen = () => { + if (store.sending) return + if (!on()) setStore("customOn", store.tab, true) + setStore("editing", true) + customUpdate(input(), true) + } + const selectOption = (optIndex: number) => { if (store.sending) return if (optIndex === options().length) { - setStore("editing", true) + customOpen() return } @@ -112,176 +201,225 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => pick(opt.label) } - const handleCustomSubmit = (e: Event) => { - e.preventDefault() + const commitCustom = () => { + setStore("editing", false) + customUpdate(input()) + } + + const next = () => { if (store.sending) return + if (store.editing) commitCustom() - const value = input().trim() - if (!value) { - setStore("editing", false) + if (store.tab >= total() - 1) { + submit() return } - if (multi()) { - setStore("answers", store.tab, (current = []) => { - if (current.includes(value)) return current - return [...current, value] - }) - setStore("editing", false) - return - } + setStore("tab", store.tab + 1) + setStore("editing", false) + } - pick(value, true) + const back = () => { + if (store.sending) return + if (store.tab <= 0) return + setStore("tab", store.tab - 1) + setStore("editing", false) + } + + const jump = (tab: number) => { + if (store.sending) return + setStore("tab", tab) setStore("editing", false) } return ( -
- -
- - {(q, index) => { - const active = () => index() === store.tab - const answered = () => (store.answers[index()]?.length ?? 0) > 0 - return ( + (root = el)} + header={ + <> +
{summary()}
+
+ + {(_, i) => ( - ) - }} - - -
- - - -
-
- {question()?.question} - {multi() ? " " + language.t("ui.question.multiHint") : ""} -
-
- - {(opt, i) => { - const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false - return ( - - ) - }} + onClick={() => jump(i())} + aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`} + /> + )} +
+ + } + footer={ + <> + +
+ 0}> + + + +
+ + } + > +
{question()?.question}
+ {language.t("ui.question.singleHint")}
}> +
{language.t("ui.question.multiHint")}
+
+
+ + {(opt, i) => { + const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false + return ( + + ) + }} + + + selectOption(options().length)} + onClick={customOpen} > - {language.t("ui.messagePart.option.typeOwnAnswer")} - - {input()} - - - - + + + {language.t("ui.messagePart.option.typeOwnAnswer")} + {input() || language.t("ui.question.custom.placeholder")} + - -
- setTimeout(() => el.focus(), 0)} - type="text" - data-slot="custom-input" - placeholder={language.t("ui.question.custom.placeholder")} - value={input()} - disabled={store.sending} - onInput={(e) => { - setStore("custom", store.tab, e.currentTarget.value) - }} - /> - - -
-
-
-
-
- - -
-
{language.t("ui.messagePart.review.title")}
- - {(q, index) => { - const value = () => store.answers[index()]?.join(", ") ?? "" - const answered = () => Boolean(value()) - return ( -
- {q.question} - - {answered() ? value() : language.t("ui.question.review.notAnswered")} - -
- ) + } + > +
{ + if (store.sending) { + e.preventDefault() + return + } + if (e.target instanceof HTMLTextAreaElement) return + const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]') + if (input instanceof HTMLTextAreaElement) input.focus() }} - -
-
- -
- - - - - - - - + + }> + + + + + + {language.t("ui.messagePart.option.typeOwnAnswer")} +
` diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts index 5ca355d1d..995f6eb19 100644 --- a/packages/app/src/pages/session/helpers.ts +++ b/packages/app/src/pages/session/helpers.ts @@ -35,6 +35,27 @@ export const createOpenReviewFile = (input: { } } +export const createOpenSessionFileTab = (input: { + normalizeTab: (tab: string) => string + openTab: (tab: string) => void + pathFromTab: (tab: string) => string | undefined + loadFile: (path: string) => void + openReviewPanel: () => void + setActive: (tab: string) => void +}) => { + return (value: string) => { + const next = input.normalizeTab(value) + input.openTab(next) + + const path = input.pathFromTab(next) + if (!path) return + + input.loadFile(path) + input.openReviewPanel() + input.setActive(next) + } +} + export const getTabReorderIndex = (tabs: readonly string[], from: string, to: string) => { const fromIndex = tabs.indexOf(from) const toIndex = tabs.indexOf(to) diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index d5f04ccf9..c65e2600e 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -1,13 +1,21 @@ -import { For, onCleanup, onMount, Show, type JSX } from "solid-js" +import { For, createEffect, createMemo, on, onCleanup, Show, type JSX } from "solid-js" +import { createStore, produce } from "solid-js/store" +import { useNavigate, useParams } from "@solidjs/router" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" +import { Dialog } from "@opencode-ai/ui/dialog" import { InlineInput } from "@opencode-ai/ui/inline-input" -import { Tooltip } from "@opencode-ai/ui/tooltip" import { SessionTurn } from "@opencode-ai/ui/session-turn" import type { UserMessage } from "@opencode-ai/sdk/v2" +import { showToast } from "@opencode-ai/ui/toast" import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture" +import { SessionContextUsage } from "@/components/session-context-usage" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useLanguage } from "@/context/language" +import { useSDK } from "@/context/sdk" +import { useSync } from "@/context/sync" const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => { const current = target instanceof Element ? target : undefined @@ -53,29 +61,7 @@ export function MessageTimeline(props: { isDesktop: boolean onScrollSpyScroll: () => void onAutoScrollInteraction: (event: MouseEvent) => void - showHeader: boolean centered: boolean - title?: string - parentID?: string - openTitleEditor: () => void - closeTitleEditor: () => void - saveTitleEditor: () => void | Promise - titleRef: (el: HTMLInputElement) => void - titleState: { - draft: string - editing: boolean - saving: boolean - menuOpen: boolean - pendingRename: boolean - } - onTitleDraft: (value: string) => void - onTitleMenuOpen: (open: boolean) => void - onTitlePendingRename: (value: boolean) => void - onNavigateParent: () => void - sessionID: string - onArchiveSession: (sessionID: string) => void - onDeleteSession: (sessionID: string) => void - t: (key: string, vars?: Record) => string setContentRef: (el: HTMLDivElement) => void turnStart: number onRenderEarlier: () => void @@ -86,13 +72,234 @@ export function MessageTimeline(props: { anchor: (id: string) => string onRegisterMessage: (el: HTMLDivElement, id: string) => void onUnregisterMessage: (id: string) => void - onFirstTurnMount?: () => void lastUserMessageID?: string - expanded: Record - onToggleExpanded: (id: string) => void }) { let touchGesture: number | undefined + const params = useParams() + const navigate = useNavigate() + const sdk = useSDK() + const sync = useSync() + const dialog = useDialog() + const language = useLanguage() + + const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) + const sessionID = createMemo(() => params.id) + const info = createMemo(() => { + const id = sessionID() + if (!id) return + return sync.session.get(id) + }) + const titleValue = createMemo(() => info()?.title) + const parentID = createMemo(() => info()?.parentID) + const showHeader = createMemo(() => !!(titleValue() || parentID())) + + const [title, setTitle] = createStore({ + draft: "", + editing: false, + saving: false, + menuOpen: false, + pendingRename: false, + }) + let titleRef: HTMLInputElement | undefined + + const errorMessage = (err: unknown) => { + if (err && typeof err === "object" && "data" in err) { + const data = (err as { data?: { message?: string } }).data + if (data?.message) return data.message + } + if (err instanceof Error) return err.message + return language.t("common.requestFailed") + } + + createEffect( + on( + sessionKey, + () => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }), + { defer: true }, + ), + ) + + const openTitleEditor = () => { + if (!sessionID()) return + setTitle({ editing: true, draft: titleValue() ?? "" }) + requestAnimationFrame(() => { + titleRef?.focus() + titleRef?.select() + }) + } + + const closeTitleEditor = () => { + if (title.saving) return + setTitle({ editing: false, saving: false }) + } + + const saveTitleEditor = async () => { + const id = sessionID() + if (!id) return + if (title.saving) return + + const next = title.draft.trim() + if (!next || next === (titleValue() ?? "")) { + setTitle({ editing: false, saving: false }) + return + } + + setTitle("saving", true) + await sdk.client.session + .update({ sessionID: id, title: next }) + .then(() => { + sync.set( + produce((draft) => { + const index = draft.session.findIndex((s) => s.id === id) + if (index !== -1) draft.session[index].title = next + }), + ) + setTitle({ editing: false, saving: false }) + }) + .catch((err) => { + setTitle("saving", false) + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err), + }) + }) + } + + const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => { + if (params.id !== sessionID) return + if (parentID) { + navigate(`/${params.dir}/session/${parentID}`) + return + } + if (nextSessionID) { + navigate(`/${params.dir}/session/${nextSessionID}`) + return + } + navigate(`/${params.dir}/session`) + } + + const archiveSession = async (sessionID: string) => { + const session = sync.session.get(sessionID) + if (!session) return + + const sessions = sync.data.session ?? [] + const index = sessions.findIndex((s) => s.id === sessionID) + const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) + + await sdk.client.session + .update({ sessionID, time: { archived: Date.now() } }) + .then(() => { + sync.set( + produce((draft) => { + const index = draft.session.findIndex((s) => s.id === sessionID) + if (index !== -1) draft.session.splice(index, 1) + }), + ) + navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) + }) + .catch((err) => { + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err), + }) + }) + } + + const deleteSession = async (sessionID: string) => { + const session = sync.session.get(sessionID) + if (!session) return false + + const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived) + const index = sessions.findIndex((s) => s.id === sessionID) + const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) + + const result = await sdk.client.session + .delete({ sessionID }) + .then((x) => x.data) + .catch((err) => { + showToast({ + title: language.t("session.delete.failed.title"), + description: errorMessage(err), + }) + return false + }) + + if (!result) return false + + sync.set( + produce((draft) => { + const removed = new Set([sessionID]) + + const byParent = new Map() + for (const item of draft.session) { + const parentID = item.parentID + if (!parentID) continue + const existing = byParent.get(parentID) + if (existing) { + existing.push(item.id) + continue + } + byParent.set(parentID, [item.id]) + } + + const stack = [sessionID] + while (stack.length) { + const parentID = stack.pop() + if (!parentID) continue + + const children = byParent.get(parentID) + if (!children) continue + + for (const child of children) { + if (removed.has(child)) continue + removed.add(child) + stack.push(child) + } + } + + draft.session = draft.session.filter((s) => !removed.has(s.id)) + }), + ) + + navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) + return true + } + + const navigateParent = () => { + const id = parentID() + if (!id) return + navigate(`/${params.dir}/session/${id}`) + } + + function DialogDeleteSession(props: { sessionID: string }) { + const name = createMemo(() => sync.session.get(props.sessionID)?.title ?? language.t("command.session.new")) + const handleDelete = async () => { + await deleteSession(props.sessionID) + dialog.close() + } + + return ( + +
+
+ + {language.t("session.delete.confirm", { name: name() })} + +
+
+ + +
+
+
+ ) + } + return (
- +
-
-
- +
+
+ - + - {props.title} +

+ {titleValue()}

} > props.onTitleDraft(event.currentTarget.value)} + ref={(el) => { + titleRef = el + }} + value={title.draft} + disabled={title.saving} + class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]" + style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }} + onInput={(event) => setTitle("draft", event.currentTarget.value)} onKeyDown={(event) => { event.stopPropagation() if (event.key === "Enter") { event.preventDefault() - void props.saveTitleEditor() + void saveTitleEditor() return } if (event.key === "Escape") { event.preventDefault() - props.closeTitleEditor() + closeTitleEditor() } }} - onBlur={props.closeTitleEditor} + onBlur={closeTitleEditor} />
- + {(id) => ( -
- - - - +
+ + setTitle("menuOpen", open)} + > + { - if (!props.titleState.pendingRename) return + if (!title.pendingRename) return event.preventDefault() - props.onTitlePendingRename(false) - props.openTitleEditor() + setTitle("pendingRename", false) + openTitleEditor() }} > { - props.onTitlePendingRename(true) - props.onTitleMenuOpen(false) + setTitle("pendingRename", true) + setTitle("menuOpen", false) }} > - {props.t("common.rename")} + {language.t("common.rename")} - props.onArchiveSession(id())}> - {props.t("common.archive")} + void archiveSession(id())}> + {language.t("common.archive")} - props.onDeleteSession(id())}> - {props.t("common.delete")} + dialog.show(() => )} + > + {language.t("common.delete")} @@ -263,7 +484,7 @@ export function MessageTimeline(props: {
0}>
@@ -288,45 +509,37 @@ export function MessageTimeline(props: { onClick={props.onLoadEarlier} > {props.historyLoading - ? props.t("session.messages.loadingEarlier") - : props.t("session.messages.loadEarlier")} + ? language.t("session.messages.loadingEarlier") + : language.t("session.messages.loadEarlier")}
- {(message) => { - if (import.meta.env.DEV && props.onFirstTurnMount) { - onMount(() => props.onFirstTurnMount?.()) - } - - return ( -
{ - props.onRegisterMessage(el, message.id) - onCleanup(() => props.onUnregisterMessage(message.id)) + {(message) => ( +
{ + props.onRegisterMessage(el, message.id) + onCleanup(() => props.onUnregisterMessage(message.id)) + }} + classList={{ + "min-w-0 w-full max-w-full": true, + "md:max-w-200 2xl:max-w-[1000px]": props.centered, + }} + > + - props.onToggleExpanded(message.id)} - classes={{ - root: "min-w-0 w-full relative", - content: "flex flex-col justify-between !overflow-visible", - container: "w-full px-4 md:px-6", - }} - /> -
- ) - }} + /> +
+ )}
diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index 634491c72..3a9f63949 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -144,8 +144,8 @@ export function SessionReviewTab(props: SessionReviewTabProps) { onOpenChange={props.view().review.setOpen} classes={{ root: props.classes?.root ?? "pb-6", - header: props.classes?.header ?? "px-6", - container: props.classes?.container ?? "px-6", + header: props.classes?.header ?? "px-3", + container: props.classes?.container ?? "px-3", }} diffs={props.diffs()} diffStyle={props.diffStyle} diff --git a/packages/app/src/pages/session/session-mobile-tabs.tsx b/packages/app/src/pages/session/session-mobile-tabs.tsx index 73aebc079..f97199b49 100644 --- a/packages/app/src/pages/session/session-mobile-tabs.tsx +++ b/packages/app/src/pages/session/session-mobile-tabs.tsx @@ -1,5 +1,6 @@ import { Show } from "solid-js" import { Tabs } from "@opencode-ai/ui/tabs" +import { useLanguage } from "@/context/language" export function SessionMobileTabs(props: { open: boolean @@ -8,8 +9,9 @@ export function SessionMobileTabs(props: { reviewCount: number onSession: () => void onChanges: () => void - t: (key: string, vars?: Record) => string }) { + const language = useLanguage() + return ( @@ -20,7 +22,7 @@ export function SessionMobileTabs(props: { classes={{ button: "w-full" }} onClick={props.onSession} > - {props.t("session.tab.session")} + {language.t("session.tab.session")} {props.hasReview - ? props.t("session.review.filesChanged", { count: props.reviewCount }) - : props.t("session.review.change.other")} + ? language.t("session.review.filesChanged", { count: props.reviewCount }) + : language.t("session.review.change.other")} diff --git a/packages/app/src/pages/session/session-prompt-dock.tsx b/packages/app/src/pages/session/session-prompt-dock.tsx index 8ec4f3b9f..abe12bcb0 100644 --- a/packages/app/src/pages/session/session-prompt-dock.tsx +++ b/packages/app/src/pages/session/session-prompt-dock.tsx @@ -1,31 +1,180 @@ -import { For, Show } from "solid-js" -import type { QuestionRequest } from "@opencode-ai/sdk/v2" +import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js" +import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2" +import { useParams } from "@solidjs/router" import { Button } from "@opencode-ai/ui/button" -import { BasicTool } from "@opencode-ai/ui/basic-tool" +import { DockPrompt } from "@opencode-ai/ui/dock-prompt" +import { Icon } from "@opencode-ai/ui/icon" +import { showToast } from "@opencode-ai/ui/toast" import { PromptInput } from "@/components/prompt-input" import { QuestionDock } from "@/components/question-dock" -import { questionSubtitle } from "@/pages/session/session-prompt-helpers" +import { SessionTodoDock } from "@/components/session-todo-dock" +import { useGlobalSync } from "@/context/global-sync" +import { useLanguage } from "@/context/language" +import { usePrompt } from "@/context/prompt" +import { useSDK } from "@/context/sdk" +import { useSync } from "@/context/sync" +import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff" export function SessionPromptDock(props: { centered: boolean - questionRequest: () => QuestionRequest | undefined - permissionRequest: () => { patterns: string[]; permission: string } | undefined - blocked: boolean - promptReady: boolean - handoffPrompt?: string - t: (key: string, vars?: Record) => string - responding: boolean - onDecide: (response: "once" | "always" | "reject") => void inputRef: (el: HTMLDivElement) => void newSessionWorktree: string onNewSessionWorktreeReset: () => void onSubmit: () => void setPromptDockRef: (el: HTMLDivElement) => void }) { + const params = useParams() + const sdk = useSDK() + const sync = useSync() + const globalSync = useGlobalSync() + const prompt = usePrompt() + const language = useLanguage() + + const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) + const handoffPrompt = createMemo(() => getSessionHandoff(sessionKey())?.prompt) + + const todos = createMemo((): Todo[] => { + const id = params.id + if (!id) return [] + return globalSync.data.session_todo[id] ?? [] + }) + + const questionRequest = createMemo((): QuestionRequest | undefined => { + const sessionID = params.id + if (!sessionID) return + return sync.data.question[sessionID]?.[0] + }) + + const permissionRequest = createMemo((): PermissionRequest | undefined => { + const sessionID = params.id + if (!sessionID) return + return sync.data.permission[sessionID]?.[0] + }) + + const blocked = createMemo(() => !!permissionRequest() || !!questionRequest()) + + const previewPrompt = () => + prompt + .current() + .map((part) => { + if (part.type === "file") return `[file:${part.path}]` + if (part.type === "agent") return `@${part.name}` + if (part.type === "image") return `[image:${part.filename}]` + return part.content + }) + .join("") + .trim() + + createEffect(() => { + if (!prompt.ready()) return + setSessionHandoff(sessionKey(), { prompt: previewPrompt() }) + }) + + const [responding, setResponding] = createSignal() + const permissionResponding = () => { + const perm = permissionRequest() + if (!perm) return false + return responding() === perm.id + } + + const decide = (response: "once" | "always" | "reject") => { + const perm = permissionRequest() + if (!perm) return + if (responding() === perm.id) return + + setResponding(perm.id) + sdk.client.permission + .respond({ sessionID: perm.sessionID, permissionID: perm.id, response }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("common.requestFailed"), description: message }) + }) + .finally(() => { + setResponding((id) => (id === perm.id ? undefined : id)) + }) + } + + const done = createMemo( + () => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"), + ) + + const [dock, setDock] = createSignal(todos().length > 0) + const [closing, setClosing] = createSignal(false) + const [opening, setOpening] = createSignal(false) + let timer: number | undefined + let raf: number | undefined + + const scheduleClose = () => { + if (timer) window.clearTimeout(timer) + timer = window.setTimeout(() => { + setDock(false) + setClosing(false) + timer = undefined + }, 400) + } + + createEffect( + on( + () => [todos().length, done()] as const, + ([count, complete], prev) => { + if (raf) cancelAnimationFrame(raf) + raf = undefined + + if (count === 0) { + if (timer) window.clearTimeout(timer) + timer = undefined + setDock(false) + setClosing(false) + setOpening(false) + return + } + + if (!complete) { + if (timer) window.clearTimeout(timer) + timer = undefined + const wasHidden = !dock() || closing() + setDock(true) + setClosing(false) + if (wasHidden) { + setOpening(true) + raf = requestAnimationFrame(() => { + setOpening(false) + raf = undefined + }) + return + } + setOpening(false) + return + } + + if (prev && prev[1]) { + if (closing() && !timer) scheduleClose() + return + } + + setDock(true) + setOpening(false) + setClosing(true) + scheduleClose() + }, + ), + ) + + onCleanup(() => { + if (!timer) return + window.clearTimeout(timer) + }) + + onCleanup(() => { + if (!raf) return + cancelAnimationFrame(raf) + }) + return (
- + {(req) => { - const subtitle = questionSubtitle(req.questions.length, (key) => props.t(key)) return ( -
- +
) }} - - {(perm) => ( -
- - 0}> -
- - {(pattern) => {pattern}} - -
-
- -
- {props.t("settings.permissions.tool.doom_loop.description")} -
-
-
-
-
- - - -
+ + {(perm) => { + const toolDescription = () => { + const key = `settings.permissions.tool.${perm.permission}.description` + const value = language.t(key as Parameters[0]) + if (value === key) return "" + return value + } + + return ( +
+ + + + +
{language.t("notification.permission.title")}
+
+ } + footer={ + <> +
+
+ + + +
+ + } + > + +
+
+
+ + 0}> +
+
+
+
-
- )} + ) + }} - + - {props.handoffPrompt || props.t("prompt.loading")} + {handoffPrompt() || language.t("prompt.loading")}
} > - + +
+ +
+
+
+ +
diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 33954f64a..07b18f314 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -1,156 +1,268 @@ -import { For, Match, Show, Switch, createMemo, onCleanup, type JSX, type ValidComponent } from "solid-js" +import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js" +import { createStore } from "solid-js/store" +import { createMediaQuery } from "@solid-primitives/media" +import { useParams } from "@solidjs/router" import { Tabs } from "@opencode-ai/ui/tabs" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Mark } from "@opencode-ai/ui/logo" +import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" +import type { DragEvent } from "@thisbeyond/solid-dnd" +import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" +import { useDialog } from "@opencode-ai/ui/context/dialog" + import FileTree from "@/components/file-tree" import { SessionContextUsage } from "@/components/session-context-usage" -import { SessionContextTab, SortableTab, FileVisual } from "@/components/session" import { DialogSelectFile } from "@/components/dialog-select-file" -import { createFileTabListSync } from "@/pages/session/file-tab-scroll" -import { FileTabContent } from "@/pages/session/file-tabs" -import { StickyAddButton } from "@/pages/session/review-tab" -import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" -import { ConstrainDragYAxis } from "@/utils/solid-dnd" -import type { DragEvent } from "@thisbeyond/solid-dnd" -import { useComments } from "@/context/comments" +import { SessionContextTab, SortableTab, FileVisual } from "@/components/session" import { useCommand } from "@/context/command" -import { useDialog } from "@opencode-ai/ui/context/dialog" import { useFile, type SelectedLineRange } from "@/context/file" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { useSync } from "@/context/sync" -import type { Message, UserMessage } from "@opencode-ai/sdk/v2/client" - -type SessionSidePanelViewModel = { - messages: () => Message[] - visibleUserMessages: () => UserMessage[] - view: () => ReturnType["view"]> - info: () => ReturnType["session"]["get"]> -} +import { createFileTabListSync } from "@/pages/session/file-tab-scroll" +import { FileTabContent } from "@/pages/session/file-tabs" +import { createOpenSessionFileTab, getTabReorderIndex } from "@/pages/session/helpers" +import { StickyAddButton } from "@/pages/session/review-tab" +import { setSessionHandoff } from "@/pages/session/handoff" export function SessionSidePanel(props: { - open: boolean - reviewOpen: boolean - language: ReturnType - layout: ReturnType - command: ReturnType - dialog: ReturnType - file: ReturnType - comments: ReturnType - hasReview: boolean - reviewCount: number - reviewTab: boolean - contextOpen: () => boolean - openedTabs: () => string[] - activeTab: () => string - activeFileTab: () => string | undefined - tabs: () => ReturnType["tabs"]> - openTab: (value: string) => void - showAllFiles: () => void reviewPanel: () => JSX.Element - vm: SessionSidePanelViewModel - handoffFiles: () => Record | undefined - codeComponent: NonNullable - addCommentToContext: (input: { - file: string - selection: SelectedLineRange - comment: string - preview?: string - origin?: "review" | "file" - }) => void - activeDraggable: () => string | undefined - onDragStart: (event: unknown) => void - onDragEnd: () => void - onDragOver: (event: DragEvent) => void - fileTreeTab: () => "changes" | "all" - setFileTreeTabValue: (value: string) => void - diffsReady: boolean - diffFiles: string[] - kinds: Map activeDiff?: string focusReviewDiff: (path: string) => void }) { - const openedTabs = createMemo(() => props.openedTabs()) + const params = useParams() + const layout = useLayout() + const sync = useSync() + const file = useFile() + const language = useLanguage() + const command = useCommand() + const dialog = useDialog() + + const isDesktop = createMediaQuery("(min-width: 768px)") + const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) + const tabs = createMemo(() => layout.tabs(sessionKey)) + const view = createMemo(() => layout.view(sessionKey)) + + const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) + const open = createMemo(() => isDesktop() && (view().reviewPanel.opened() || layout.fileTree.opened())) + const reviewTab = createMemo(() => isDesktop() && !layout.fileTree.opened()) + + const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) + const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) + const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length)) + const hasReview = createMemo(() => reviewCount() > 0) + const diffsReady = createMemo(() => { + const id = params.id + if (!id) return true + if (!hasReview()) return true + return sync.data.session_diff[id] !== undefined + }) + + const diffFiles = createMemo(() => diffs().map((d) => d.file)) + const kinds = createMemo(() => { + const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => { + if (!a) return b + if (a === b) return a + return "mix" as const + } + + const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "") + + const out = new Map() + for (const diff of diffs()) { + const file = normalize(diff.file) + const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix" + + out.set(file, kind) + + const parts = file.split("/") + for (const [idx] of parts.slice(0, -1).entries()) { + const dir = parts.slice(0, idx + 1).join("/") + if (!dir) continue + out.set(dir, merge(out.get(dir), kind)) + } + } + return out + }) + + const normalizeTab = (tab: string) => { + if (!tab.startsWith("file://")) return tab + return file.tab(tab) + } + + const openReviewPanel = () => { + if (!view().reviewPanel.opened()) view().reviewPanel.open() + } + + const openTab = createOpenSessionFileTab({ + normalizeTab, + openTab: tabs().open, + pathFromTab: file.pathFromTab, + loadFile: file.load, + openReviewPanel, + setActive: tabs().setActive, + }) + + const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context")) + const openedTabs = createMemo(() => + tabs() + .all() + .filter((tab) => tab !== "context" && tab !== "review"), + ) + + const activeTab = createMemo(() => { + const active = tabs().active() + if (active === "context") return "context" + if (active === "review" && reviewTab()) return "review" + if (active && file.pathFromTab(active)) return normalizeTab(active) + + const first = openedTabs()[0] + if (first) return first + if (contextOpen()) return "context" + if (reviewTab() && hasReview()) return "review" + return "empty" + }) + + const activeFileTab = createMemo(() => { + const active = activeTab() + if (!openedTabs().includes(active)) return + return active + }) + + const fileTreeTab = () => layout.fileTree.tab() + + const setFileTreeTabValue = (value: string) => { + if (value !== "changes" && value !== "all") return + layout.fileTree.setTab(value) + } + + const showAllFiles = () => { + if (fileTreeTab() !== "changes") return + layout.fileTree.setTab("all") + } + + const [store, setStore] = createStore({ + activeDraggable: undefined as string | undefined, + }) + + const handleDragStart = (event: unknown) => { + const id = getDraggableId(event) + if (!id) return + setStore("activeDraggable", id) + } + + const handleDragOver = (event: DragEvent) => { + const { draggable, droppable } = event + if (!draggable || !droppable) return + + const currentTabs = tabs().all() + const toIndex = getTabReorderIndex(currentTabs, draggable.id.toString(), droppable.id.toString()) + if (toIndex === undefined) return + tabs().move(draggable.id.toString(), toIndex) + } + + const handleDragEnd = () => { + setStore("activeDraggable", undefined) + } + + createEffect(() => { + if (!file.ready()) return + + setSessionHandoff(sessionKey(), { + files: tabs() + .all() + .reduce>((acc, tab) => { + const path = file.pathFromTab(tab) + if (!path) return acc + + const selected = file.selectedLines(path) + acc[path] = + selected && typeof selected === "object" && "start" in selected && "end" in selected + ? (selected as SelectedLineRange) + : null + + return acc + }, {}), + }) + }) return ( - +