diff --git a/.changeset/close-other-tabs.md b/.changeset/close-other-tabs.md new file mode 100644 index 00000000000..a35c8e3ca6b --- /dev/null +++ b/.changeset/close-other-tabs.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Support closing other Agent Manager tabs from the tab context menu. diff --git a/.changeset/fix-llm-user-agent.md b/.changeset/fix-llm-user-agent.md new file mode 100644 index 00000000000..f5609c9ee41 --- /dev/null +++ b/.changeset/fix-llm-user-agent.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Identify Kilo in provider request user-agent headers instead of OpenCode. diff --git a/.changeset/highlight-changed-characters.md b/.changeset/highlight-changed-characters.md new file mode 100644 index 00000000000..c53dcde1074 --- /dev/null +++ b/.changeset/highlight-changed-characters.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Highlight the exact changed characters within modified diff lines. diff --git a/.changeset/ime-enter-composition-guard.md b/.changeset/ime-enter-composition-guard.md new file mode 100644 index 00000000000..f4e0e871716 --- /dev/null +++ b/.changeset/ime-enter-composition-guard.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Ignore Enter while IME composition is active (including Windows virtual-key 229) so composed text is not accidentally submitted from chat and sidebar inputs. diff --git a/.changeset/move-worktrees-safely.md b/.changeset/move-worktrees-safely.md new file mode 100644 index 00000000000..e77ccea53c2 --- /dev/null +++ b/.changeset/move-worktrees-safely.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Clean up incomplete worktrees when moving a session fails to transfer its Git changes. diff --git a/.changeset/name-compact-settings-tabs.md b/.changeset/name-compact-settings-tabs.md new file mode 100644 index 00000000000..e462423736b --- /dev/null +++ b/.changeset/name-compact-settings-tabs.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Announce every Settings tab by name to screen readers in compact sidebars. diff --git a/.changeset/prevent-atomic-chat-install.md b/.changeset/prevent-atomic-chat-install.md new file mode 100644 index 00000000000..03367e39a72 --- /dev/null +++ b/.changeset/prevent-atomic-chat-install.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent the bundled Atomic Chat plugin from triggering an npm installation. diff --git a/.changeset/render-image-diffs.md b/.changeset/render-image-diffs.md new file mode 100644 index 00000000000..7445a209c7d --- /dev/null +++ b/.changeset/render-image-diffs.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Render image changes in diff viewers and open images with VS Code's image preview. diff --git a/.changeset/restore-authenticated-speech-input.md b/.changeset/restore-authenticated-speech-input.md new file mode 100644 index 00000000000..6f826ad0660 --- /dev/null +++ b/.changeset/restore-authenticated-speech-input.md @@ -0,0 +1,7 @@ +--- +"@kilocode/cli": patch +"@kilocode/sdk": patch +"kilo-code": patch +--- + +Restore speech input when profile details are unavailable, move transcription model selection to the Models tab, and default transcription to Whisper Large V3 Turbo. diff --git a/.changeset/search-agent-manager-sidebar.md b/.changeset/search-agent-manager-sidebar.md new file mode 100644 index 00000000000..e47bae5fc41 --- /dev/null +++ b/.changeset/search-agent-manager-sidebar.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Search and switch between local sessions, worktrees, and their sessions from the Agent Manager sidebar. diff --git a/.changeset/share-worktree-indexes.md b/.changeset/share-worktree-indexes.md new file mode 100644 index 00000000000..fcb6db6bf68 --- /dev/null +++ b/.changeset/share-worktree-indexes.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": minor +"@kilocode/kilo-indexing": minor +--- + +Share the main codebase index with Agent Manager worktrees while indexing and searching only each worktree's changed files. diff --git a/.changeset/show-edit-approval-line-numbers.md b/.changeset/show-edit-approval-line-numbers.md new file mode 100644 index 00000000000..4dbb443f3d2 --- /dev/null +++ b/.changeset/show-edit-approval-line-numbers.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Show line numbers in edit approval diffs, including compact sidebar views. diff --git a/.changeset/steady-codex-refresh.md b/.changeset/steady-codex-refresh.md new file mode 100644 index 00000000000..2d8438541e1 --- /dev/null +++ b/.changeset/steady-codex-refresh.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent concurrent Kilo processes from reusing a ChatGPT Codex refresh token. diff --git a/.changeset/switch-model-agent-links.md b/.changeset/switch-model-agent-links.md new file mode 100644 index 00000000000..0aad2e12d12 --- /dev/null +++ b/.changeset/switch-model-agent-links.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Support VS Code switch links that select a Kilo model, agent, or both. diff --git a/.changeset/terminal-title-status.md b/.changeset/terminal-title-status.md new file mode 100644 index 00000000000..4234657571b --- /dev/null +++ b/.changeset/terminal-title-status.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": minor +"@kilocode/sdk": minor +--- + +Add opt-in Unicode or emoji terminal title indicators for sessions that are working, need attention, or have finished. diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index 797252b7fd8..d8b3485035b 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -3,7 +3,7 @@ name: containers on: push: branches: - - dev + - main # kilocode_change paths: - packages/containers/** - .github/workflows/containers.yml diff --git a/.github/workflows/test-jetbrains.yml b/.github/workflows/test-jetbrains.yml new file mode 100644 index 00000000000..7fde0ec1ea3 --- /dev/null +++ b/.github/workflows/test-jetbrains.yml @@ -0,0 +1,109 @@ +# kilocode_change - new file +name: test-jetbrains + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + checks: write + pull-requests: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + changes: + name: detect JetBrains changes + runs-on: blacksmith-4vcpu-ubuntu-2404 + outputs: + jetbrains: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.filter.outputs.jetbrains }} + steps: + - name: Checkout repository + if: github.event_name != 'workflow_dispatch' + uses: actions/checkout@v6 + + - name: Detect JetBrains changes + if: github.event_name != 'workflow_dispatch' + id: filter + uses: Kilo-Org/paths-filter@668c092af3649c4b664c54e4b704aa46782f6f7c # v3 + with: + predicate-quantifier: every + filters: | + jetbrains: + - '**' + - '!packages/kilo-vscode/**' + - '!packages/kilo-docs/**' + + unit: + name: jetbrains + needs: changes + if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.jetbrains == 'true' + runs-on: blacksmith-4vcpu-ubuntu-2404 + defaults: + run: + shell: bash + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + + - name: Run JetBrains unit tests + run: bun script/test-ci.ts + working-directory: packages/kilo-jetbrains + + - name: Publish JetBrains unit reports + if: always() + uses: mikepenz/action-junit-report@bccf2e31636835cf0874589931c4116687171386 # v6.4.0 + with: + report_paths: packages/kilo-jetbrains/.artifacts/unit/junit.xml + check_name: "unit results (jetbrains)" + detailed_summary: true + include_time_in_summary: true + fail_on_failure: false + + - name: Upload JetBrains unit artifacts + if: always() + uses: actions/upload-artifact@v7 + with: + name: unit-jetbrains-${{ github.run_attempt }} + include-hidden-files: true + if-no-files-found: ignore + retention-days: 7 + path: packages/kilo-jetbrains/.artifacts/unit/junit.xml + + required: + name: result + needs: + - changes + - unit + if: always() + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - name: Verify JetBrains jobs passed + run: | + echo "changes=${{ needs.changes.result }}" + echo "jetbrains=${{ needs.unit.result }}" + test "${{ needs.changes.result }}" = "success" + if [ "${{ needs.changes.outputs.jetbrains }}" = "true" ]; then + test "${{ needs.unit.result }}" = "success" + else + test "${{ needs.changes.outputs.jetbrains }}" = "false" + test "${{ needs.unit.result }}" = "skipped" + fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 57c021238ed..7c3b6b5a136 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,28 +22,6 @@ env: jobs: # kilocode_change start - changes: - name: detect JetBrains changes - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - pull-requests: read - outputs: - jetbrains: ${{ steps.filter.outputs.jetbrains }} - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Detect JetBrains changes - id: filter - uses: Kilo-Org/paths-filter@668c092af3649c4b664c54e4b704aa46782f6f7c # v3 - with: - predicate-quantifier: every - filters: | - jetbrains: - - '**' - - '!packages/kilo-vscode/**' - - '!packages/kilo-docs/**' unit: name: unit (${{ matrix.settings.name }}) strategy: @@ -131,55 +109,11 @@ jobs: # kilocode_change start jetbrains: name: jetbrains - needs: changes - if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.jetbrains == 'true' - runs-on: blacksmith-4vcpu-ubuntu-2404 - defaults: - run: - shell: bash - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: "21" - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - with: - cache-read-only: ${{ github.ref != 'refs/heads/main' }} - - - name: Run JetBrains unit tests - run: bun script/test-ci.ts - working-directory: packages/kilo-jetbrains - - - name: Publish JetBrains unit reports - if: always() - uses: mikepenz/action-junit-report@v6 - with: - report_paths: packages/kilo-jetbrains/.artifacts/unit/junit.xml - check_name: "unit results (jetbrains)" - detailed_summary: true - include_time_in_summary: true - fail_on_failure: false - - - name: Upload JetBrains unit artifacts - if: always() - uses: actions/upload-artifact@v7 - with: - name: unit-jetbrains-${{ github.run_attempt }} - include-hidden-files: true - if-no-files-found: ignore - retention-days: 7 - path: packages/kilo-jetbrains/.artifacts/unit/junit.xml + permissions: + contents: read + checks: write + pull-requests: read + uses: ./.github/workflows/test-jetbrains.yml # kilocode_change end # kilocode_change start @@ -187,22 +121,14 @@ jobs: name: test (linux) runs-on: blacksmith-4vcpu-ubuntu-2404 needs: - - changes - unit - jetbrains if: always() steps: - name: Verify upstream test jobs passed run: | - echo "changes=${{ needs.changes.result }}" echo "unit=${{ needs.unit.result }}" echo "jetbrains=${{ needs.jetbrains.result }}" - test "${{ needs.changes.result }}" = "success" test "${{ needs.unit.result }}" = "success" - if [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ needs.changes.outputs.jetbrains }}" = "true" ]; then - test "${{ needs.jetbrains.result }}" = "success" - else - test "${{ needs.changes.outputs.jetbrains }}" = "false" - test "${{ needs.jetbrains.result }}" = "skipped" - fi + test "${{ needs.jetbrains.result }}" = "success" # kilocode_change end diff --git a/.kilo/skills/release-jetbrains/SKILL.md b/.kilo/skills/release-jetbrains/SKILL.md index 1d749bc416a..32da47c8c3a 100644 --- a/.kilo/skills/release-jetbrains/SKILL.md +++ b/.kilo/skills/release-jetbrains/SKILL.md @@ -58,7 +58,13 @@ Create a changelog draft after the prepare PR exists: 1. Read the PR body with `gh pr view --json body`. 2. Extract `JetBrains-From-Tag`, `JetBrains-Tag`, and `## Generated Notes`. -3. Use the release range and path filter as the primary relevance signal: +3. Fetch the release range tags if they are missing locally: + +```bash +git fetch origin refs/tags/:refs/tags/ refs/tags/:refs/tags/ +``` + +4. Use the release range and path filter as the primary relevance signal: ```bash git log --oneline .. -- packages/opencode packages/kilo-jetbrains @@ -103,9 +109,33 @@ The script updates `packages/kilo-jetbrains/CHANGELOG.md` on `jetbrains/release/ docs(jetbrains): edit changelog for v ``` +If `update-changelog.ts` fails with `gh: Not Found (HTTP 404)`, verify the release branch and changelog path with: + +```bash +gh api "repos/Kilo-Org/kilocode/contents/packages/kilo-jetbrains/CHANGELOG.md?ref=jetbrains/release/v" +``` + +Then either fix and retry the helper, or perform the equivalent contents API update using `ref` in the query string. + ## Approve And Publish -Ask the user to approve the release changelog and metadata. By default, have the user merge the release PR manually in GitHub, then watch the publish workflow: +Ask the user to approve the release changelog and metadata. Before merging or publishing, verify the PR approval and required checks are green: + +```bash +gh pr view --json mergeStateStatus,reviewDecision,statusCheckRollup +gh pr checks --watch --interval 10 +``` + +Do not merge or publish while required checks are failing unless the user explicitly gives a maintainer override. + +If a required check fails from an apparent flake, rerun only the failed jobs and wait for the run to finish: + +```bash +gh run rerun --failed +gh run watch --exit-status +``` + +By default, have the user merge the release PR manually in GitHub, then watch the publish workflow: ```bash bun .kilo/skills/release-jetbrains/script/watch-publish.ts --pr --version 7.0.1-rc.7 @@ -123,11 +153,18 @@ Pass a generous Bash timeout, such as `1800000` ms. If the shell times out, re-a bun .kilo/skills/release-jetbrains/script/watch-publish.ts --pr --version 7.0.1-rc.7 --run-id ``` +If `watch-publish.ts --merge` reports that the PR is already merged, or a transient GitHub API `5xx` interrupts publish-run discovery, rerun without `--merge`: + +```bash +bun .kilo/skills/release-jetbrains/script/watch-publish.ts --pr --version +``` + Report the Marketplace channel and GitHub Release URL. RC versions publish to the `eap` channel; stable versions publish to the default Marketplace channel. ## Recovery - If prepare created the tag but failed before creating a PR, rerun prepare for the same version. The existing workflow reuses the tag if it points to the same commit. - If a tag points to an unexpected SHA, stop and inspect manually. Do not move or delete release tags casually. +- If release PR checks fail from an apparent flake, use `gh run rerun --failed`, then `gh run watch --exit-status` before publishing. - If publish fails after merge, rerun the failed workflow only if Marketplace did not already accept the version. - If Marketplace succeeds but GitHub Release upload fails, manually create or edit the GitHub Release for `jetbrains/v` using the reviewed changelog. diff --git a/.kilo/skills/release-jetbrains/script/update-changelog.ts b/.kilo/skills/release-jetbrains/script/update-changelog.ts index b6a3ff071fc..c8fba6db0f9 100644 --- a/.kilo/skills/release-jetbrains/script/update-changelog.ts +++ b/.kilo/skills/release-jetbrains/script/update-changelog.ts @@ -30,7 +30,7 @@ const branch = `jetbrains/release/v${ver}` const section = strip((await Bun.file(file).text()).trim()) validate(section, ver) -const current = (await $`gh api ${`repos/${repo}/contents/${path}`} -f ref=${branch}`.json()) as { +const current = (await $`gh api ${`repos/${repo}/contents/${path}?ref=${encodeURIComponent(branch)}`}`.json()) as { content: string encoding: string sha: string diff --git a/.kilo/skills/release-jetbrains/script/watch-publish.ts b/.kilo/skills/release-jetbrains/script/watch-publish.ts index 0719bcb2071..17130256e13 100644 --- a/.kilo/skills/release-jetbrains/script/watch-publish.ts +++ b/.kilo/skills/release-jetbrains/script/watch-publish.ts @@ -37,10 +37,14 @@ console.log(`runUrl=${url}`) await $`gh run watch ${id} --repo ${repo} --exit-status` -const rel = (await $`gh release view ${`jetbrains/v${ver}`} --repo ${repo} --json url,isPrerelease`.json()) as { - url: string - isPrerelease: boolean -} +const rel = await retry( + async () => + (await $`gh release view ${`jetbrains/v${ver}`} --repo ${repo} --json url,isPrerelease`.json()) as { + url: string + isPrerelease: boolean + }, + "view release", +) console.log( JSON.stringify( { @@ -56,7 +60,15 @@ console.log( async function merge() { const before = new Set((await runs()).map((run) => run.databaseId)) - await $`gh pr merge ${pr} --repo ${repo} --merge` + try { + await $`gh pr merge ${pr} --repo ${repo} --merge` + } catch (err) { + if (await merged()) { + console.warn(`PR ${pr} is already merged; looking for the publish workflow run`) + return await find() + } + throw err + } for (const _ of Array.from({ length: 120 })) { const run = (await runs()).find((item) => item.headBranch === branch && !before.has(item.databaseId)) @@ -68,30 +80,49 @@ async function merge() { async function find() { for (const _ of Array.from({ length: 120 })) { - const run = (await runs()).find((item) => item.headBranch === branch && active(item.status)) + const run = (await runs()).find((item) => item.headBranch === branch) if (run) return String(run.databaseId) await Bun.sleep(1000) } - throw new Error( - `No ${workflow} run found for ${branch}. Merge PR ${pr} first, or pass --merge to merge it automatically.`, - ) + throw new Error(`No ${workflow} run found for ${branch}. Merge PR ${pr} first, or pass --merge to merge it automatically.`) } -function active(status: string) { - return ( - status === "queued" || - status === "in_progress" || - status === "waiting" || - status === "requested" || - status === "pending" +async function merged() { + const info = await retry( + async () => (await $`gh pr view ${pr} --repo ${repo} --json state`.json()) as { state: string }, + "check PR state", ) + return info.state === "MERGED" } async function runs() { - return (await $`gh run list --repo ${repo} --workflow ${workflow} --event pull_request --json databaseId,createdAt,headBranch,status --limit 100`.json()) as { - databaseId: number - createdAt: string - headBranch: string - status: string - }[] + return await retry( + async () => + (await $`gh run list --repo ${repo} --workflow ${workflow} --event pull_request --json databaseId,createdAt,headBranch,status --limit 100`.json()) as { + databaseId: number + createdAt: string + headBranch: string + status: string + }[], + "list workflow runs", + ) +} + +async function retry(task: () => Promise, label: string, tries = 5): Promise { + try { + return await task() + } catch (err) { + if (tries <= 1 || !transient(err)) throw err + console.warn(`${label} failed with a transient GitHub error; retrying (${tries - 1} left): ${message(err)}`) + await Bun.sleep(2000) + return await retry(task, label, tries - 1) + } +} + +function transient(err: unknown) { + return /\bHTTP 5\d\d\b|\b50[234]\b|Bad Gateway|Gateway Timeout|Service Unavailable/i.test(message(err)) +} + +function message(err: unknown) { + return err instanceof Error ? err.message : String(err) } diff --git a/AGENTS.md b/AGENTS.md index 56c25b60c1c..1a332992b49 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang - **Dev**: `bun run dev` (runs from root) or `bun run --cwd packages/opencode --conditions=browser src/index.ts` - **Dev with params**: `bun dev -- help` - **Extension**: `bun run extension` (build + launch VS Code with the extension in dev mode). Pass `--no-build` to skip the build. -- **Typecheck**: `bun turbo typecheck` (uses `tsgo`, not `tsc`). Includes the JetBrains plugin — requires Java 21. Check with `java -version` before running. If missing, install via SDKMAN: `sdk install java 21-tem && sdk use java 21-tem`. If SDKMAN is not installed, see https://sdkman.io/install. +- **Typecheck**: `bun turbo typecheck` (uses `tsgo`, not `tsc`). Includes the JetBrains plugin and requires Java 21; do not run `java -version` as a routine preflight. Only check Java when a Gradle/Java command fails with a Java-version or missing-Java error. If missing, install via SDKMAN: `sdk install java 21-tem && sdk use java 21-tem`. If SDKMAN is not installed, see https://sdkman.io/install. - **Test**: `bun test` from `packages/opencode/` (NOT from root -- root blocks tests) - **Single test**: `bun test ./test/tool/tool-define.test.ts` from `packages/opencode/` - **CLI build artifact size check**: after `bun run script/build.ts --single --skip-install` in `packages/opencode/`, use `du -h dist/*/*/bin/kilo` (scoped package output lives under `dist/@kilocode/`) @@ -35,7 +35,7 @@ Before saying an implementation is ready, run the smallest relevant checks that | CLI | From `packages/opencode/`: `bun run typecheck`, `bun test` or targeted `bun test ./path/to/file.test.ts` | | VS Code extension | From `packages/kilo-vscode/`: `bun run typecheck`, `bun run lint`, `bun run test:unit` or `bun run test` | | Extension build/package | From `packages/kilo-vscode/`: `bun run compile` or `bun run package` when touching build, packaging, SDK, or webview integration paths | -| JetBrains plugin | From `packages/kilo-jetbrains/`: `./gradlew typecheck`, `./gradlew test`. Requires Java 21 — check first with `java -version`. Install via SDKMAN if missing: `sdk install java 21-tem && sdk use java 21-tem`. | +| JetBrains plugin | From `packages/kilo-jetbrains/`: `./gradlew typecheck`, `./gradlew test`. Requires Java 21; do not run `java -version` as a routine preflight. Check Java only after a Java-version or missing-Java failure. | | CI-only guards | Run affected guards documented above, such as `bun run knip`, `bun run check-kilocode-change`, `bun run script/check-opencode-annotations.ts`, or source link extraction | Never run root `bun test`; the root script prints `do not run tests from root` and exits with code 1. Use package-level tests instead. diff --git a/README.md b/README.md index 31ef5436e17..5fa853fb440 100644 --- a/README.md +++ b/README.md @@ -1,144 +1,165 @@ +

+ Kilo Code logo +

+ +

The open source coding agent for building with AI in VS Code, JetBrains, or the CLI.

+

VS Code Marketplace + npm X (Twitter) - Substack Blog + Blog Discord Reddit

-

- kilo-code-logo -

+![Kilo-in-VS-Code-and-CLI](https://github.com/user-attachments/assets/0536ca59-ed81-4512-9e05-d186187a1b52) -

- Kilo is the all-in-one agentic engineering platform.
- Build, ship, and iterate faster with the most popular open source coding agent. -

+--- -

- Kilo Code running inside VS Code -

+Kilo Code is an AI coding agent that meets you everywhere you work: [VS Code](https://kilo.ai/landing/vs-code), [JetBrains](https://kilo.ai/features/jetbrains-native), and the [CLI](https://kilo.ai/cli). It's open source with open pricing. You pick from 500+ models, switch between them mid-task, and pay the model provider's rate with zero markup. No API keys required to start. -

- Website · - Install · - IDE · - CLI · - Docs · - Models · - Gateway · - Pricing · - Kilo Pass -

+### Installation -

- 500+ models. One open source agent in VS Code, JetBrains, CLI, Slack, and Cloud. -

+Pick where you want to run Kilo. -- ✨ Generate code from natural language -- ✅ Checks its own work -- 🧪 Run terminal commands -- 🌐 Automate the browser -- ⚡ Inline autocomplete suggestions -- 🤖 Latest AI models -- 🎁 API keys optional +
+VS Code -## Quick Links +
-- [VS Code Marketplace](https://kilo.ai/vscode-marketplace?utm_source=Readme) (download) -- Install CLI: `npm install -g @kilocode/cli` -- [Official Kilo.ai Home page](https://kilo.ai) (learn more) +Install the [Kilo Code extension](vscode:extension/kilocode.kilo-code) directly, or grab it from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code). Create an account and you'll have access to 500+ models including GPT-5.5, Claude Opus 4.7, Claude Sonnet 4.6, and Gemini 3.1 Pro Preview, all at provider pricing. -## Key Features +
-- **Code Generation:** Kilo can generate code using natural language. -- **Inline Autocomplete:** Get intelligent code completions as you type, powered by AI. -- **Task Automation:** Kilo can automate repetitive coding tasks to save time. -- **Automated Refactoring:** Kilo can refactor and improve existing code efficiently. -- **MCP Server Marketplace**: Kilo can easily find, and use MCP servers to extend the agent capabilities. -- **Multi Mode**: Plan with Architect, Code with Coder, and Debug with Debugger, and make your own custom modes. +
+CLI -## Get Started in Visual Studio Code - -1. Install the Kilo Code extension from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code). -2. Create your account to access 500+ cutting-edge AI models including GPT-5.5, Claude Opus 4.7, Claude Sonnet 4.6, and Gemini 3.1 Pro Preview, with transparent pricing that matches provider rates exactly. -3. Start coding with AI that adapts to your workflow. Watch our quick-start guide to see Kilo in action: - -Watch the video - -## Get Started with the CLI +
```bash # npm npm install -g @kilocode/cli -# Or run directly with npx -npx @kilocode/cli +# curl +curl -fsSL https://kilo.ai/cli/install | bash + +# pnpm +pnpm add -g @kilocode/cli + +# bun +bun add -g @kilocode/cli + +# Homebrew (macOS / Linux) +brew install Kilo-Org/tap/kilo + +# Arch Linux (AUR) +paru -S kilo-bin ``` Then run `kilo` in any project directory to start. - +
-### npm Install Note: Hidden `.kilo` File +
+JetBrains -On some systems and npm versions, installing `@kilocode/cli` can create a hidden `.kilo` file near the installed `kilo` command (for example in a global npm bin directory). This file is an npm-generated launcher helper, not project data. +
-- Why it exists: npm may create helper artifacts while wiring CLI executables. -- Size caveat: size can vary by platform, npm version, and install mode (symlink vs copied launcher), so a strict fixed size is not guaranteed. -- Safety: it is safe to leave in place. Do not edit it manually. Use your package manager's uninstall (`npm uninstall -g @kilocode/cli`) to remove install artifacts cleanly. - +Install the [Kilo Code plugin](https://plugins.jetbrains.com/plugin/28350-kilo-code) from the JetBrains Marketplace, or search "Kilo Code" in `Settings → Plugins` inside any JetBrains IDE. -### Install from GitHub Releases (Optional) +
-Download the latest binary or source code from the [Releases page](https://github.com/Kilo-Org/kilocode/releases), use this quick guide: +
+Cloud Agent -- `kilo--.zip` is the CLI binary for your OS and CPU architecture on Windows and macOS. (`kilo-linux-.tar.gz` for Linux) -- `darwin` means macOS. -- `x64` is standard 64-bit Intel/AMD CPUs. -- `x64-baseline` is a compatibility build for older x64 CPUs(do not support AVX Instruction). -- `arm64` is ARM-based Linux/MacOS. -- `musl` is statically linked Linux build for Alpine/minimal Docker without glibc. Alpine/minimal Docker users should prefer the matching \*-musl asset. -- `kilo-vscode-*.vsix` is the VS Code extension package and not the CLI binary. -- `Source code` releases are for building from source, not normal installation. +
-For most users: +Run Kilo from the web, no local machine needed, at [app.kilo.ai/cloud](https://app.kilo.ai/cloud). -- **Windows (most PCs):** `kilo-windows-x64.zip` -- **macOS Apple Silicon:** `kilo-darwin-arm64.zip` -- **macOS Intel:** `kilo-darwin-x64.zip` -- **Linux x64:** `kilo-linux-x64.tar.gz` -- **Linux on ARM:** `kilo-linux-arm64.tar.gz` +
+ +
+Code Reviews + +
+ +Set up automated AI code reviews on your pull requests at [app.kilo.ai/code-reviews](https://app.kilo.ai/code-reviews). + +
+ +
+KiloClaw + +
+ +Spin up your always-on AI agent at [app.kilo.ai/claw](https://app.kilo.ai/claw). + +
+ +
+Install the CLI from GitHub Releases (binaries) + +Download the latest binary from the [Releases page](https://github.com/Kilo-Org/kilocode/releases). + +| Platform | Asset | +|---|---| +| Windows (most PCs) | `kilo-windows-x64.zip` | +| macOS (Apple Silicon) | `kilo-darwin-arm64.zip` | +| macOS (Intel) | `kilo-darwin-x64.zip` | +| Linux x64 | `kilo-linux-x64.tar.gz` | +| Linux ARM | `kilo-linux-arm64.tar.gz` | + +Notes: `x64-baseline` is a compatibility build for older CPUs without AVX. `musl` is the statically linked build for Alpine or minimal Docker images without glibc. `kilo-vscode-*.vsix` is the VS Code extension package, not the CLI. `Source code` archives are for building from source. + +
+ +### Agents + +Kilo ships with specialized agents you switch between depending on the task. You can also build your own custom agents. + +- **Code** - The default. Implements and edits code from natural language. +- **Plan** - Designs architecture and writes implementation plans before any code gets written. +- **Ask** - Answers questions about your codebase without touching any files. +- **Debug** - Troubleshoots and traces issues. +- **Review** - Reviews your changes and surfaces issues across performance, security, style, and test coverage. + +Learn more about [agents and custom agents](https://kilo.ai/docs/code-with-ai/agents/using-agents). + +### What it does + +- **Code generation** from natural language, across multiple files. +- **Inline autocomplete** with ghost-text suggestions and tab to accept. +- **Self-checking** so the agent reviews and corrects its own work. +- **Terminal and browser control** to run commands and automate the web. +- **MCP marketplace** to find and wire up MCP servers that extend what the agent can do. +- **500+ models** with mid-task switching, so you can match latency, cost, and reasoning to the job. ### Autonomous Mode (CI/CD) -Use the `--auto` flag with `kilo run` to enable fully autonomous operation without user interaction. This is ideal for CI/CD pipelines and automated workflows: +Run `kilo run` with `--auto` for fully autonomous operation with no prompts, built for CI/CD pipelines: ```bash kilo run --auto "run tests and fix any failures" ``` -**Important:** The `--auto` flag disables all permission prompts and allows the agent to execute any action without confirmation. Only use this in trusted environments like CI/CD pipelines. +`--auto` disables all permission prompts and lets the agent execute any action without confirmation. Only use it in trusted environments. -## Contributing +### Documentation -We welcome contributions from developers, writers, and enthusiasts! -To get started, please read our [Contributing Guide](/CONTRIBUTING.md). It includes details on setting up your environment, coding standards, types of contribution and how to submit pull requests. +For configuration and everything else, [head over to the docs](https://kilo.ai/docs). -See [RELEASING.md](RELEASING.md) for the VS Code extension and CLI release process. +### Contributing -See [packages/kilo-jetbrains/RELEASING.md](packages/kilo-jetbrains/RELEASING.md) for the JetBrains plugin release process. +Contributions are welcome from developers, writers, and everyone in between. Start with the [Contributing Guide](/CONTRIBUTING.md) for environment setup, coding standards, and how to open a pull request. See [RELEASING.md](RELEASING.md) for the VS Code extension and CLI release process, and [packages/kilo-jetbrains/RELEASING.md](packages/kilo-jetbrains/RELEASING.md) for the JetBrains plugin. -## Code of Conduct +Please review our [Code of Conduct](/CODE_OF_CONDUCT.md) before getting involved. -Our community is built on respect, inclusivity, and collaboration. Please review our [Code of Conduct](/CODE_OF_CONDUCT.md) to understand the expectations for all contributors and community members. +### License -## License +MIT. You're free to use, modify, and distribute this code, including commercially, as long as you keep the attribution and license notices. See [License](/LICENSE). -This project is licensed under the MIT License. -You’re free to use, modify, and distribute this code, including for commercial purposes as long as you include proper attribution and license notices. See [License](/LICENSE). - -## FAQ +### FAQ
Where did Kilo CLI come from? @@ -146,3 +167,7 @@ You’re free to use, modify, and distribute this code, including for commercial Kilo CLI is a fork of [OpenCode](https://github.com/anomalyco/opencode), enhanced to work within the Kilo agentic engineering platform.
+ +--- + +**Join the community** [Discord](https://kilo.ai/discord) | [X](https://x.com/kilocode) | [Reddit](https://www.reddit.com/r/kilocode/) diff --git a/bun.lock b/bun.lock index b47af1c7e9b..2c0e923620f 100644 --- a/bun.lock +++ b/bun.lock @@ -5,16 +5,12 @@ "": { "name": "@kilocode/kilo", "dependencies": { - "@aws-sdk/client-s3": "3.1025.0", "@kilocode/plugin": "workspace:*", "@kilocode/sdk": "workspace:*", - "@morphllm/morphsdk": "0.2.166", "@opencode-ai/script": "workspace:*", - "heap-snapshot-toolkit": "1.1.3", "typescript": "catalog:", }, "devDependencies": { - "@actions/artifact": "5.0.1", "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.27.10", "@tsconfig/bun": "catalog:", @@ -27,7 +23,6 @@ "oxlint-tsgolint": "0.21.0", "prettier": "3.6.2", "semver": "^7.6.0", - "sst": "3.18.10", "turbo": "2.9.14", }, }, @@ -91,7 +86,6 @@ "@types/cross-spawn": "catalog:", "@types/npm-package-arg": "6.1.4", "@types/npmcli__arborist": "6.3.3", - "@types/semver": "catalog:", }, }, "packages/http-recorder": { @@ -114,7 +108,6 @@ "@kilocode/kilo-web-ui": "workspace:*", "@kilocode/sdk": "workspace:*", "@lottiefiles/dotlottie-web": "0.74.0", - "@opencode-ai/ui": "workspace:*", "@solidjs/router": "catalog:", "ghostty-web": "0.4.0", "solid-js": "catalog:", @@ -170,7 +163,6 @@ "@ai-sdk/openai-compatible": "2.0.48", "@clack/prompts": "1.0.0-alpha.1", "@kilocode/plugin": "workspace:*", - "@kilocode/sdk": "workspace:*", "@openrouter/ai-sdk-provider": "2.9.0", "ai": "catalog:", "open": "10.1.2", @@ -313,7 +305,6 @@ "@xterm/addon-web-links": "0.12.0", "@xterm/xterm": "6.0.0", "diff": "8.0.4", - "dotenv": "^16.4.7", "fastest-levenshtein": "^1.0.16", "friendly-words": "1.3.1", "fuzzysort": "3.1.0", @@ -366,6 +357,8 @@ "dependencies": { "@kilocode/kilo-ui": "workspace:*", "@kobalte/core": "catalog:", + "@opencode-ai/ui": "workspace:*", + "@pierre/diffs": "catalog:", "solid-js": "catalog:", }, "devDependencies": { @@ -430,10 +423,6 @@ "@effect/platform-node": "catalog:", "@gitlab/gitlab-ai-provider": "3.6.0", "@gitlab/opencode-gitlab-auth": "1.3.3", - "@hono/node-server": "1.19.13", - "@hono/node-ws": "1.3.0", - "@hono/standard-validator": "0.1.5", - "@hono/zod-validator": "catalog:", "@kilocode/kilo-gateway": "workspace:*", "@kilocode/kilo-indexing": "workspace:*", "@kilocode/kilo-telemetry": "workspace:*", @@ -444,36 +433,29 @@ "@modelcontextprotocol/sdk": "1.29.0", "@morphllm/morphsdk": "0.2.166", "@npmcli/arborist": "9.4.0", - "@npmcli/config": "10.8.1", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", - "@openauthjs/openauth": "catalog:", "@opencode-ai/script": "workspace:*", "@opencode-ai/ui": "workspace:*", "@openrouter/ai-sdk-provider": "2.9.0", "@opentelemetry/api": "1.9.0", - "@opentelemetry/context-async-hooks": "2.6.1", - "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", "@opentui/core": "catalog:", "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", "@parcel/watcher": "2.5.1", - "@pierre/diffs": "catalog:", "@secretlint/core": "10.2.2", "@secretlint/secretlint-rule-preset-recommend": "10.2.2", "@silvia-odwyer/photon-node": "0.3.4", "@solid-primitives/event-bus": "1.1.2", "@solid-primitives/scheduled": "1.5.2", - "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", "ai-gateway-provider": "3.1.2", "bonjour-service": "1.3.0", "bun-pty": "0.4.8", "chardet": "2.1.1", - "chokidar": "4.0.3", "cli-sound": "1.1.3", "clipboardy": "4.0.0", "cross-spawn": "catalog:", @@ -483,11 +465,8 @@ "effect": "catalog:", "fuzzysort": "3.1.0", "gitlab-ai-provider": "6.6.0", - "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", - "hono": "catalog:", - "hono-openapi": "catalog:", "htmlparser2": "8.0.2", "iconv-lite": "0.7.2", "ignore": "7.0.5", @@ -495,16 +474,13 @@ "jsonc-parser": "3.3.1", "mammoth": "1.12.0", "mime-types": "3.0.2", - "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "open": "10.1.2", "opencode-gitlab-auth": "2.0.1", "opencode-poe-auth": "0.0.1", "opentui-spinner": "catalog:", - "partial-json": "0.1.7", "remeda": "catalog:", "ripgrep": "0.3.1", - "rotating-file-stream": "3.2.9", "semver": "^7.6.3", "simple-git": "3.36.0", "solid-js": "catalog:", @@ -518,14 +494,11 @@ "vscode-jsonrpc": "8.2.1", "web-tree-sitter": "0.25.10", "which": "6.0.1", - "xdg-basedir": "5.1.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "yargs": "18.0.0", "zod": "catalog:", - "zod-to-json-schema": "3.24.5", }, "devDependencies": { - "@babel/core": "7.28.4", "@effect/language-service": "0.84.2", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/core": "workspace:*", @@ -538,9 +511,7 @@ "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1", - "@standard-schema/spec": "1.0.0", "@tsconfig/bun": "catalog:", - "@types/babel__core": "7.20.5", "@types/bun": "catalog:", "@types/cross-spawn": "catalog:", "@types/mime-types": "3.0.1", @@ -558,7 +529,6 @@ "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", - "zod-to-json-schema": "3.24.5", }, }, "packages/plugin": { @@ -660,8 +630,6 @@ "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", "@pierre/diffs": "catalog:", - "@shikijs/transformers": "3.9.2", - "@solid-primitives/bounds": "0.1.3", "@solid-primitives/event-listener": "2.4.5", "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", @@ -672,10 +640,7 @@ "dompurify": "3.4.2", "fuzzysort": "catalog:", "katex": "0.16.27", - "luxon": "catalog:", "marked": "catalog:", - "marked-katex-extension": "5.1.6", - "marked-shiki": "catalog:", "mermaid": "11.15.0", "morphdom": "2.7.8", "motion": "12.34.5", @@ -694,7 +659,6 @@ "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", "@types/katex": "0.16.7", - "@types/luxon": "catalog:", "@typescript/native-preview": "catalog:", "tailwindcss": "catalog:", "typescript": "catalog:", @@ -741,7 +705,6 @@ "@cloudflare/workers-types": "4.20251008.0", "@effect/opentelemetry": "4.0.0-beta.65", "@effect/platform-node": "4.0.0-beta.65", - "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", "@lydell/node-pty": "1.2.0-beta.10", "@npmcli/arborist": "9.4.0", @@ -794,15 +757,13 @@ "zod": "4.1.8", }, "packages": { - "@actions/artifact": ["@actions/artifact@5.0.1", "", { "dependencies": { "@actions/core": "^2.0.0", "@actions/github": "^6.0.1", "@actions/http-client": "^3.0.0", "@azure/storage-blob": "^12.29.1", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-dHJ5rHduhCKUikKTT9eXeWoUvfKia3IjR1sO/VTAV3DVAL4yMTRnl2iO5mcfiBjySHLwPNezwENAVskKYU5ymw=="], - "@actions/core": ["@actions/core@1.11.1", "", { "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" } }, "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A=="], "@actions/exec": ["@actions/exec@1.1.1", "", { "dependencies": { "@actions/io": "^1.0.1" } }, "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w=="], "@actions/github": ["@actions/github@6.0.1", "", { "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", "@octokit/plugin-paginate-rest": "^9.2.2", "@octokit/plugin-rest-endpoint-methods": "^10.4.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "undici": "^5.28.5" } }, "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw=="], - "@actions/http-client": ["@actions/http-client@3.0.2", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^6.23.0" } }, "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA=="], + "@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], "@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], @@ -870,10 +831,6 @@ "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], - "@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="], - - "@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="], - "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], @@ -886,12 +843,8 @@ "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1025.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-node": "^3.972.29", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ke7vyS7Dmo3St1a354AlpAjocZpG25Ql52XQ1AXRD3VQ791FBT7vX+EqGCfAEIWLPxpcldaW2Nny6P6klEOkkw=="], - "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1025.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-node": "^3.972.29", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", "@aws-sdk/middleware-flexible-checksums": "^3.974.6", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-sdk-s3": "^3.972.27", "@aws-sdk/middleware-ssec": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/signature-v4-multi-region": "^3.996.15", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-blob-browser": "^4.2.13", "@smithy/hash-node": "^4.2.12", "@smithy/hash-stream-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-9Byz2fPnuGRRL8DTTD5bYPl1Iwm+ysLiCMgptffa3lNkVLCiUZc5e5TAaOjk0MvyeXieq+jn35AmQL6cgN2KHQ=="], - "@aws-sdk/core": ["@aws-sdk/core@3.974.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.18", "@smithy/core": "^3.23.15", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-gy/gffKz0zaHDaqRiLCdIvgHmaAL/HXuAtMcBP7euYSFx4BsbsdlfmUBJag+Gqe62z6/XuloKyQyaiH+kS3Vrg=="], - "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.7", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-QUagVVBbC8gODCF6e1aV0mE2TXWB9Opz4k8EJFdNrujUVQm5R4AjJa1mpOqzwOuROBzqJU9zawzig7M96L8Ejg=="], - "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.24", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-i6eMWlKfgQkNY3S/kg1ZnBZm2lhd6r8B3yobCalvrfCCGjvthREwsyDViuWl7gOWSvuUjQEFFGcJhGbCstcqJg=="], "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.27", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-xfUt2CUZDC+Tf16A6roD1b4pk/nrXdkoLY3TEhv198AXDtBo5xUJP1zd0e8SmuKLN4PpIBX96OizZbmMlcI6oQ=="], @@ -914,26 +867,14 @@ "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.14", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/eventstream-codec": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-m4X56gxG76/CKfxNVbOFuYwnAZcHgS6HOH8lgp15HoGHIAVTcZfZrXvcYzJFOMLEJgVn+JHBu6EiNV+xSNXXFg=="], - "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Vbc2frZH7wXlMNd+ZZSXUEs/l1Sv8Jj4zUnIfwrYF5lwaLdXHZ9xx4U3rjUcaye3HRhFVc+E5DbBxpRAbB16BA=="], - "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-QUqLs7Af1II9X4fCRAu+EGHG3KHyOp4RkuLhRKoA3NuFlh6TL8i+zXBl8w2LUxqm44B/Kom45hgSlwA1SpTsXQ=="], - "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-2Yn0f1Qiq/DjxYR3wfI3LokXnjOhFM7Ssn4LTdFDIxRMCE6I32MAsVnhPX1cUZsuVA9tiZtwwhlSLAtFGxAZlQ=="], - - "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.974.9", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.974.1", "@aws-sdk/crc64-nvme": "^3.972.7", "@aws-sdk/types": "^3.973.8", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ye6xVuMEQ5NCT+yQOryGYsuCXnOwu7iGFGzV+qpXZOWtqXIAAaFostapxj6RCubw36rekVwmdB2lcspFuyNfYQ=="], - "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg=="], - "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ=="], - "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ=="], "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ=="], - "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.30", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.15", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-hoQRxjJu4tt3gEOQin21rJKotClJC+x7AmCh9ylRct1DJeaNI/BRlFxMbuhJe54bG6xANPagSs0my8K30QyV9g=="], - - "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw=="], - "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.7", "@smithy/core": "^3.23.15", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-retry": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-L+hXN2HDomlIsWSHW5DVD7ppccCeRnlHXZ5uHG34ePTjF5bm0I1fmrJLbUGiW97xRXWryit5cjdP4Sx2FwiGog=="], "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.16", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-format-url": "^3.972.10", "@smithy/eventstream-codec": "^4.2.14", "@smithy/eventstream-serde-browser": "^4.2.14", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-86+S9oCyRVGzoMRpQhxkArp7kD2K75GPmaNevd9B6EyNhWoNvnCZZ3WbgN4j7ZT+jvtvBCGZvI2XHsWZJ+BRIg=="], @@ -942,14 +883,10 @@ "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.12", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/config-resolver": "^4.4.16", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-QQI43Mxd53nBij0pm8HXC+t4IOC6gnhhZfzxE0OATQyO6QfPV4e+aTIRRuAJKA6Nig/cR8eLwPryqYTX9ZrjAQ=="], - "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.18", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.30", "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-4KT8UXRmvNAP5zKq9UI1MIwbnmSChZncBt89RKu/skMqZSSWGkBZTAJsZ+no+txfmF3kVaUFv31CTBZkQ5BJpQ=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1005.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/nested-clients": "^3.996.8", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-vMxd+ivKqSxU9bHx5vmAlFKDAkjGotFU56IOkDa5DaTu1WWwbcse0yFHEm9I537oVvodaiwMl3VBwgHfzQ2rvw=="], "@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="], - "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-endpoints": "^3.4.1", "tslib": "^2.6.2" } }, "sha512-ty4LQxN1QC+YhUP28NfEgZDEGXkyqOQy+BDriBozqHsrYO4JMgiPhfizqOGF7P+euBTZ5Ez6SKlLAMCLo8tzmw=="], "@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ=="], @@ -990,8 +927,6 @@ "@azure/core-util": ["@azure/core-util@1.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A=="], - "@azure/core-xml": ["@azure/core-xml@1.5.1", "", { "dependencies": { "fast-xml-parser": "^5.5.9", "tslib": "^2.8.1" } }, "sha512-xcNRHqCoSp4AunOALEae6A8f3qATb83gSrm31Iqb01OzblvC3/W/bfXozcq78EzIdzZzuH1bZ2NvRR0TdX709w=="], - "@azure/identity": ["@azure/identity@4.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^5.1.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw=="], "@azure/keyvault-common": ["@azure/keyvault-common@2.1.0", "", { "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-rest-pipeline": "^1.8.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.10.0", "@azure/logger": "^1.1.4", "tslib": "^2.2.0" } }, "sha512-aCDidWuKY06LWQ4x7/8TIXK6iRqTaRWRL3t7T+LC+j1b07HtoIsOxP/tU90G4jCSBn5TAyUTCtA4MS/y5Hudaw=="], @@ -1006,15 +941,11 @@ "@azure/msal-node": ["@azure/msal-node@5.1.3", "", { "dependencies": { "@azure/msal-common": "16.5.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-LqT8mRZpEils9zGR9eW+Ljqifh2aMA99UF/X0jxIKDYZeHr6onlHwhVP4xHCeLhh55BI63JCbdf1iWJbMh1mPw=="], - "@azure/storage-blob": ["@azure/storage-blob@12.31.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.3", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/core-xml": "^1.4.5", "@azure/logger": "^1.1.4", "@azure/storage-common": "^12.3.0", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg=="], - - "@azure/storage-common": ["@azure/storage-common@12.3.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.3.0", "tslib": "^2.8.1" } }, "sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ=="], - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], - "@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], + "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], @@ -1072,10 +1003,6 @@ "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], - "@bufbuild/protobuf": ["@bufbuild/protobuf@2.11.0", "", {}, "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ=="], - - "@bufbuild/protoplugin": ["@bufbuild/protoplugin@2.11.0", "", { "dependencies": { "@bufbuild/protobuf": "2.11.0", "@typescript/vfs": "^1.6.2", "typescript": "5.4.5" } }, "sha512-lyZVNFUHArIOt4W0+dwYBe5GBwbKzbOy8ObaloEqsw9Mmiwv2O48TwddDoHN4itylC+BaEGqFdI1W8WQt2vWJQ=="], - "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.1", "", { "dependencies": { "@changesets/config": "^3.1.4", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA=="], "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.10", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A=="], @@ -1250,14 +1177,10 @@ "@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="], - "@hono/node-server": ["@hono/node-server@1.19.13", "", { "peerDependencies": { "hono": "^4" } }, "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ=="], - - "@hono/node-ws": ["@hono/node-ws@1.3.0", "", { "dependencies": { "ws": "^8.17.0" }, "peerDependencies": { "@hono/node-server": "^1.19.2", "hono": "^4.6.0" } }, "sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q=="], + "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], "@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="], - "@hono/zod-validator": ["@hono/zod-validator@0.4.2", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g=="], - "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], @@ -1476,8 +1399,6 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw=="], - "@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -1528,12 +1449,10 @@ "@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="], - "@octokit/plugin-request-log": ["@octokit/plugin-request-log@1.0.4", "", { "peerDependencies": { "@octokit/core": ">=3" } }, "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA=="], + "@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="], "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@10.4.1", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg=="], - "@octokit/plugin-retry": ["@octokit/plugin-retry@3.0.9", "", { "dependencies": { "@octokit/types": "^6.0.3", "bottleneck": "^2.15.3" } }, "sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ=="], - "@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="], "@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="], @@ -1544,8 +1463,6 @@ "@octokit/webhooks-types": ["@octokit/webhooks-types@7.6.1", "", {}, "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw=="], - "@openauthjs/openauth": ["@openauthjs/openauth@0.0.0-20250322224806", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-p5IWSRXvABcwocH2dNI0w8c1QJelIOFulwhKk+aLLFfUbs8u1pr7kQbYe8yCSM2+bcLHiwbogpUQc2ovrGwCuw=="], - "@opencode-ai/core": ["@opencode-ai/core@workspace:packages/core"], "@opencode-ai/http-recorder": ["@opencode-ai/http-recorder@workspace:packages/http-recorder"], @@ -1610,16 +1527,6 @@ "@opentui/solid": ["@opentui/solid@0.2.11", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.11", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-M3WHxBFORHVE0yqMJYpi9PfjXWlnRTw/LYuBhZaJv0HTo+zTs60P/ukGcwnHDWnMpTGf3BH9x0Yi2dIqjHRY6Q=="], - "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], - - "@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="], - - "@oslojs/crypto": ["@oslojs/crypto@1.0.1", "", { "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" } }, "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ=="], - - "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], - - "@oslojs/jwt": ["@oslojs/jwt@0.2.0", "", { "dependencies": { "@oslojs/encoding": "0.4.1" } }, "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg=="], - "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.19.1", "", { "os": "android", "cpu": "arm" }, "sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg=="], "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.19.1", "", { "os": "android", "cpu": "arm64" }, "sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA=="], @@ -1752,14 +1659,6 @@ "@posthog/types": ["@posthog/types@1.369.3", "", {}, "sha512-Ywqvs4513PixR2TIA5O3GMEyK4F65uefwxPfsIUeHr9ruGylyXp00YJ4CEbp8U0DMzCkeF+LsMKVnHgN3pAXcA=="], - "@protobuf-ts/plugin": ["@protobuf-ts/plugin@2.11.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.4.0", "@bufbuild/protoplugin": "^2.4.0", "@protobuf-ts/protoc": "^2.11.1", "@protobuf-ts/runtime": "^2.11.1", "@protobuf-ts/runtime-rpc": "^2.11.1", "typescript": "^3.9" }, "bin": { "protoc-gen-ts": "bin/protoc-gen-ts", "protoc-gen-dump": "bin/protoc-gen-dump" } }, "sha512-HyuprDcw0bEEJqkOWe1rnXUP0gwYLij8YhPuZyZk6cJbIgc/Q0IFgoHQxOXNIXAcXM4Sbehh6kjVnCzasElw1A=="], - - "@protobuf-ts/protoc": ["@protobuf-ts/protoc@2.11.1", "", { "bin": { "protoc": "protoc.js" } }, "sha512-mUZJaV0daGO6HUX90o/atzQ6A7bbN2RSuHtdwo8SSF2Qoe3zHwa4IHyCN1evftTeHfLmdz+45qo47sL+5P8nyg=="], - - "@protobuf-ts/runtime": ["@protobuf-ts/runtime@2.11.1", "", {}, "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ=="], - - "@protobuf-ts/runtime-rpc": ["@protobuf-ts/runtime-rpc@2.11.1", "", { "dependencies": { "@protobuf-ts/runtime": "^2.11.1" } }, "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ=="], - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], @@ -1858,7 +1757,7 @@ "@secretlint/types": ["@secretlint/types@10.2.2", "", {}, "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg=="], - "@shikijs/core": ["@shikijs/core@3.9.2", "", { "dependencies": { "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3q/mzmw09B2B6PgFNeiaN8pkNOixWS726IHmJEpjDAcneDPMQmUg2cweT9cWXY4XcyQS3i6mOOUgQz9RRUP6HA=="], + "@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-OFx8fHAZuk7I42Z9YAdZ95To6jDePQ9Rnfbw9uSRTSbBhYBp1kEOKv/3jOimcj3VRUKusDYM6DswLauwfhboLg=="], @@ -1870,7 +1769,7 @@ "@shikijs/transformers": ["@shikijs/transformers@3.9.2", "", { "dependencies": { "@shikijs/core": "3.9.2", "@shikijs/types": "3.9.2" } }, "sha512-MW5hT4TyUp6bNAgTExRYLk1NNasVQMTCw1kgbxHcEC0O5cbepPWaB+1k+JzW9r3SP2/R8kiens8/3E6hGKfgsA=="], - "@shikijs/types": ["@shikijs/types@3.9.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw=="], + "@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], @@ -1894,10 +1793,6 @@ "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="], - "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw=="], - - "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.3", "", { "dependencies": { "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw=="], - "@smithy/config-resolver": ["@smithy/config-resolver@4.4.16", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.4.1", "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-GFlGPNLZKrGfqWpqVb31z7hvYCA9ZscfX1buYnvvMGcRYsQQnhH+4uN6mWWflcD5jB4OXP/LBrdpukEdjl41tg=="], "@smithy/core": ["@smithy/core@3.23.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-E7GVCgsQttzfujEZb6Qep005wWf4xiL4x06apFEtzQMWYBPggZh/0cnOxPficw5cuK/YjjkehKoIN4YUaSh0UQ=="], @@ -1916,18 +1811,12 @@ "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.17", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw=="], - "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.15", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-0PJ4Al3fg2nM4qKrAIxyNcApgqHAXcBkN8FeizOz69z0rb26uZ6lMESYtxegaTlXB5Hj84JfwMPavMrwDMjucA=="], - "@smithy/hash-node": ["@smithy/hash-node@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g=="], - "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-tw4GANWkZPb6+BdD4Fgucqzey2+r73Z/GRo9zklsCdwrnxxumUV83ZIaBDdudV4Ylazw3EPTiJZhpX42105ruQ=="], - "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw=="], "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], - "@smithy/md5-js": ["@smithy/md5-js@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA=="], - "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.14", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw=="], "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.30", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/middleware-serde": "^4.2.18", "@smithy/node-config-provider": "^4.3.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-qS2XqhKeXmdZ4nEQ4cOxIczSP/Y91wPAHYuRwmWDCh975B7/57uxsm5d6sisnUThn2u2FwzMdJNM7AbO1YPsPg=="], @@ -1990,14 +1879,10 @@ "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@smithy/util-waiter": ["@smithy/util-waiter@4.2.16", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-GtclrKoZ3Lt7jPQ7aTIYKfjY92OgceScftVnkTsG8e1KV8rkvZgN+ny6YSRhd9hxB8rZtwVbmln7NTvE5O3GmQ=="], - "@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], - "@solid-primitives/bounds": ["@solid-primitives/bounds@0.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/resize-observer": "^2.1.3", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UbiyKMdSPmtijcEDnYLQL3zzaejpwWDAJJ4Gt5P0hgVs6A72piov0GyNw7V2SroH7NZFwxlYS22YmOr8A5xc1Q=="], - "@solid-primitives/event-bus": ["@solid-primitives/event-bus@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="], "@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.5", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA=="], @@ -2034,7 +1919,7 @@ "@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.9", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg=="], - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@storybook/addon-a11y": ["@storybook/addon-a11y@10.2.10", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.2.10" } }, "sha512-1S9pDXgvbHhBStGarCvfJ3/rfcaiAcQHRhuM3Nk4WGSIYtC1LCSRuzYdDYU0aNRpdCbCrUA7kUCbqvIE3tH+3Q=="], @@ -2238,8 +2123,6 @@ "@types/linkify-it": ["@types/linkify-it@3.0.5", "", {}, "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw=="], - "@types/luxon": ["@types/luxon@3.7.1", "", {}, "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg=="], - "@types/markdown-it": ["@types/markdown-it@12.2.3", "", { "dependencies": { "@types/linkify-it": "*", "@types/mdurl": "*" } }, "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], @@ -2342,8 +2225,6 @@ "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260316.1", "", { "os": "win32", "cpu": "x64" }, "sha512-LvpV1hyQS0U9yMLHgWexhC7oSeBpcNbIJtYC6Iyvu63Mb6J/cP0k2fQmnAVB2yesMMQFtuY6v2YIx17vE0Ymfw=="], - "@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.5", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -2458,12 +2339,6 @@ "apache-arrow": ["apache-arrow@18.1.0", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/command-line-args": "^5.2.3", "@types/command-line-usage": "^5.0.4", "@types/node": "^20.13.0", "command-line-args": "^5.2.1", "command-line-usage": "^7.0.1", "flatbuffers": "^24.3.25", "json-bignum": "^0.0.3", "tslib": "^2.6.2" }, "bin": { "arrow2csv": "bin/arrow2csv.js" } }, "sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg=="], - "archiver": ["archiver@7.0.1", "", { "dependencies": { "archiver-utils": "^5.0.2", "async": "^3.2.4", "buffer-crc32": "^1.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", "tar-stream": "^3.0.0", "zip-stream": "^6.0.1" } }, "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ=="], - - "archiver-utils": ["archiver-utils@5.0.2", "", { "dependencies": { "glob": "^10.0.0", "graceful-fs": "^4.2.0", "is-stream": "^2.0.1", "lazystream": "^1.0.0", "lodash": "^4.17.15", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA=="], - - "arctic": ["arctic@2.3.4", "", { "dependencies": { "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@oslojs/jwt": "0.2.0" } }, "sha512-+p30BOWsctZp+CVYCt7oAean/hWGW42sH5LAcRQX56ttEkFJWbzXBhmSpibbzwSJkRrotmsA+oAoJoVsU0f5xA=="], - "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], @@ -2486,8 +2361,6 @@ "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], - "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - "async-lock": ["async-lock@1.4.1", "", {}, "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="], "async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="], @@ -2504,8 +2377,6 @@ "avvio": ["avvio@9.2.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ=="], - "aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="], - "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], "axe-core": ["axe-core@4.11.3", "", {}, "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg=="], @@ -2514,8 +2385,6 @@ "azure-devops-node-api": ["azure-devops-node-api@12.5.0", "", { "dependencies": { "tunnel": "0.0.6", "typed-rest-client": "^1.8.4" } }, "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og=="], - "b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="], - "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.6", "", { "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-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ=="], "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=="], @@ -2524,18 +2393,6 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], - - "bare-fs": ["bare-fs@4.7.1", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw=="], - - "bare-os": ["bare-os@3.8.7", "", {}, "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w=="], - - "bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="], - - "bare-stream": ["bare-stream@2.13.0", "", { "dependencies": { "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA=="], - - "bare-url": ["bare-url@2.4.1", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-fZapLWNB25gS+etK27NV9KgBNXgo2yeYHuj+OyPblQd6GYAE3JVy6aKxszMV5jhGGFwraXQKA5fldvf3lMyEqw=="], - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ=="], @@ -2548,8 +2405,6 @@ "bin-links": ["bin-links@6.0.0", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w=="], - "binary": ["binary@0.3.0", "", { "dependencies": { "buffers": "~0.1.1", "chainsaw": "~0.1.0" } }, "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg=="], - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "binaryextensions": ["binaryextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw=="], @@ -2566,8 +2421,6 @@ "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], - "boundary": ["boundary@2.0.0", "", {}, "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA=="], "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], @@ -2580,14 +2433,12 @@ "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - "buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - "buffers": ["buffers@0.1.1", "", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="], - "bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="], "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], @@ -2622,8 +2473,6 @@ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - "chainsaw": ["chainsaw@0.1.0", "", { "dependencies": { "traverse": ">=0.3.0 <0.4" } }, "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ=="], - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "chalk-template": ["chalk-template@0.4.0", "", { "dependencies": { "chalk": "^4.1.2" } }, "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg=="], @@ -2704,8 +2553,6 @@ "common-path-prefix": ["common-path-prefix@3.0.0", "", {}, "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w=="], - "compress-commons": ["compress-commons@6.0.2", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", "is-stream": "^2.0.1", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "concordance": ["concordance@5.0.4", "", { "dependencies": { "date-time": "^3.1.0", "esutils": "^2.0.3", "fast-diff": "^1.2.0", "js-string-escape": "^1.0.1", "lodash": "^4.17.15", "md5-hex": "^3.0.1", "semver": "^7.3.2", "well-known-symbols": "^2.0.0" } }, "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw=="], @@ -2736,8 +2583,6 @@ "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], - "crc32-stream": ["crc32-stream@6.0.0", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^4.0.0" } }, "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g=="], - "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -2904,7 +2749,7 @@ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="], "drizzle-kit": ["drizzle-kit@1.0.0-beta.19-d95b7a4", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-M0sqc+42TYBod6kEZ3AsW6+JWe3+76gR1aDFbHH5DmuLKEwewmbzlhBG6qnvV6YA1cIIbkuam3dC7r6PREOCXw=="], @@ -3002,8 +2847,6 @@ "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], - "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], "eventsource-parser": ["eventsource-parser@3.0.7", "", {}, "sha512-zwxwiQqexizSXFZV13zMiEtW1E3lv7RlUv+1f5FBiR4x7wFhEjm3aFTyYkZQWzyN08WnPdox015GoRH5D/E5YA=="], @@ -3038,8 +2881,6 @@ "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], - "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -3054,7 +2895,7 @@ "fast-xml-builder": ["fast-xml-builder@1.1.5", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA=="], - "fast-xml-parser": ["fast-xml-parser@5.7.1", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.5", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA=="], + "fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], @@ -3146,8 +2987,6 @@ "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], - "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], @@ -3212,8 +3051,6 @@ "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], - "heap-snapshot-toolkit": ["heap-snapshot-toolkit@1.1.3", "", {}, "sha512-joThu2rEsDu8/l4arupRDI1qP4CZXNG+J6Wr348vnbLGSiBkwRdqZ6aOHl5BzEiC+Dc8OTbMlmWjD0lbXD5K2Q=="], - "hono": ["hono@4.12.12", "", {}, "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q=="], "hono-openapi": ["hono-openapi@1.1.2", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="], @@ -3246,7 +3083,7 @@ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - "ieee754": ["ieee754@1.1.13", "", {}, "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], @@ -3280,8 +3117,6 @@ "irregular-plurals": ["irregular-plurals@3.5.0", "", {}, "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ=="], - "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], - "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], @@ -3298,8 +3133,6 @@ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], @@ -3318,8 +3151,6 @@ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], - "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], @@ -3356,9 +3187,7 @@ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - "jmespath": ["jmespath@0.16.0", "", {}, "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="], - - "jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], + "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], @@ -3420,8 +3249,6 @@ "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], - "jwt-decode": ["jwt-decode@3.1.2", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="], - "katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], "keytar": ["keytar@7.9.0", "", { "dependencies": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.0.1" } }, "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ=="], @@ -3440,8 +3267,6 @@ "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], - "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], - "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], @@ -3528,8 +3353,6 @@ "lucide-solid": ["lucide-solid@0.576.0", "", { "peerDependencies": { "solid-js": "^1.4.7" } }, "sha512-WX1JQJXCE8k4YWfj6pLx57i6AyzmH1LRyGP6LUDzIPgPIki9Pk4sIhSfTfF+jCBj8qRcp5aIz+tUURLJrzGAFw=="], - "luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="], - "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -3546,10 +3369,6 @@ "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], - "marked-katex-extension": ["marked-katex-extension@5.1.6", "", { "peerDependencies": { "katex": ">=0.16 <0.17", "marked": ">=4 <18" } }, "sha512-vYpLXwmlIDKILIhJtiRTgdyZRn5sEYdFBuTmbpjD7lbCIzg0/DWyK3HXIntN3Tp8zV6hvOUgpZNLWRCgWVc24A=="], - - "marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="], - "matcher": ["matcher@5.0.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0" } }, "sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -3622,8 +3441,6 @@ "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], - "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], @@ -3720,16 +3537,12 @@ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], - "oidc-token-hash": ["oidc-token-hash@5.2.0", "", {}, "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw=="], - "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -3752,10 +3565,6 @@ "opencode-poe-auth": ["opencode-poe-auth@0.0.1", "", { "dependencies": { "open": "^10.0.0", "poe-oauth": "*" }, "peerDependencies": { "@opencode-ai/plugin": "*" } }, "sha512-cXqTlS6AXHzo1oBdosnxbT47ZJEZ9WXn050X8Re6wZ1vaNnTpB/l2fMQt90evT7RBK0fB8UjXQUDMKyd7bbiqg=="], - "opencontrol": ["opencontrol@0.0.6", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.6.1", "@tsconfig/bun": "1.0.7", "hono": "4.7.4", "zod": "3.24.2", "zod-to-json-schema": "3.24.3" }, "bin": { "opencontrol": "bin/index.mjs" } }, "sha512-QeCrpOK5D15QV8kjnGVeD/BHFLwcVr+sn4T6KKmP0WAMs2pww56e4h+eOGHb5iPOufUQXbdbBKi6WV2kk7tefQ=="], - - "openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="], - "opentui-spinner": ["opentui-spinner@0.0.6", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-xupLOeVQEAXEvVJCvHkfX6fChDWmJIPHe5jyUrVb8+n4XVTX8mBNhitFfB9v2ZbkC1H2UwPab/ElePHoW37NcA=="], "option": ["option@0.2.4", "", {}, "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="], @@ -3814,8 +3623,6 @@ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], - "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], @@ -3946,8 +3753,6 @@ "query-selector-shadow-dom": ["query-selector-shadow-dom@1.0.1", "", {}, "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw=="], - "querystring": ["querystring@0.2.0", "", {}, "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], @@ -3984,8 +3789,6 @@ "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], @@ -4056,15 +3859,13 @@ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], - "safe-regex2": ["safe-regex2@5.1.0", "", { "dependencies": { "ret": "~0.5.0" }, "bin": { "safe-regex2": "bin/safe-regex2.js" } }, "sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw=="], "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="], + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -4182,24 +3983,6 @@ "ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="], - "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.18.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3MwIpMZhhdZKDqLp9ZQNlwkWix5+q+N0PWstuTomYwgZOxCCe6u9IIsoIszSk+GAJJN/jvGZyLiXKeV4iiQvw=="], - - "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.18.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-mj9VNj3SvLS+HaXx2PhCX0aTA7CwJNoM6JhRc0s/zCilqchcvqDjbhpYBJO4brEPv6aOaaa7T3WvIQqtYauK4Q=="], - - "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.18.10", "", { "os": "linux", "cpu": "none" }, "sha512-77qZSuPZeQ5bdRCiq1pQEdY8EcGNHboKrx4P2yFid2FBDKJsXxOXtIxJdloyx+ljBn0+nxl/g040QBmXxdc9tA=="], - - "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.18.10", "", { "os": "win32", "cpu": "x64" }, "sha512-rY+yJXOpG+P5xXnaQRpCvBK2zwwLhjzpYidGkp6F+cGgiVdh2Wre/CIQNRaVHr20ncj8lLe/RsHWa9QCNM48jg=="], - - "sst-win32-x86": ["sst-win32-x86@3.18.10", "", { "os": "win32", "cpu": "none" }, "sha512-pq8SmV0pIjBFMY6DraUZ4akyTxHnfjIKCRbBLdMxFUZK8TzA1NK2YdjRt1AwrgXRYGRyctrz/mt4WyO0SMOVQQ=="], - "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -4216,8 +3999,6 @@ "storybook-solidjs-vite": ["storybook-solidjs-vite@10.0.9", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.6.1", "@storybook/builder-vite": "^10.0.0", "@storybook/global": "^5.0.0", "vite-plugin-solid": "^2.11.8" }, "peerDependencies": { "solid-js": "^1.9.0", "storybook": "^0.0.0-0 || ^10.0.0", "typescript": ">= 4.9.x", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-n6MwWCL9mK/qIaUutE9vhGB0X1I1hVnKin2NL+iVC5oXfAiuaABVZlr/1oEeEypsgCdyDOcbEbhJmDWmaqGpPw=="], - "streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="], - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "string-width-cjs": ["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=="], @@ -4272,14 +4053,12 @@ "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], - "tar-stream": ["tar-stream@3.1.8", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ=="], + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], "tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="], "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=="], - "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="], - "temp-dir": ["temp-dir@3.0.0", "", {}, "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw=="], "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], @@ -4288,8 +4067,6 @@ "test-exclude": ["test-exclude@7.0.2", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^10.2.2" } }, "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw=="], - "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], - "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], "textextensions": ["textextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ=="], @@ -4326,8 +4103,6 @@ "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - "traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="], - "tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="], "tree-sitter-powershell": ["tree-sitter-powershell@0.25.10", "", { "dependencies": { "node-addon-api": "^7.1.0", "node-gyp-build": "^4.8.0" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-bEt8QoySpGFnU3aa8WedQyNMaN6aTwy/WUbvIVt0JSKF+BbJoSHNHu+wCbhj7xLMsfB0AuffmiJm+B8gzva8Lg=="], @@ -4406,20 +4181,14 @@ "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], - "unzip-stream": ["unzip-stream@0.3.4", "", { "dependencies": { "binary": "^0.3.0", "mkdirp": "^0.5.1" } }, "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw=="], - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="], - "url-join": ["url-join@4.0.1", "", {}, "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="], "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], - "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], @@ -4518,7 +4287,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], @@ -4536,26 +4305,16 @@ "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - "zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="], - "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], "zod-openapi": ["zod-openapi@5.4.6", "", { "peerDependencies": { "zod": "^3.25.74 || ^4.0.0" } }, "sha512-P2jsOOBAq/6hCwUsMCjUATZ8szkMsV5VAwZENfyxp2Hc/XPJQpVwAgevWZc65xZauCwWB9LAn7zYeiCJFAEL+A=="], - "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], "zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@actions/artifact/@actions/core": ["@actions/core@2.0.3", "", { "dependencies": { "@actions/exec": "^2.0.0", "@actions/http-client": "^3.0.2" } }, "sha512-Od9Thc3T1mQJYddvVPM4QGiLUewdh+3txmDYHHxoNdkqysR1MbCT+rFOtNUxYAz+7+6RIsqipVahY2GJqGPyxA=="], - - "@actions/core/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], - - "@actions/github/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], - - "@actions/http-client/undici": ["undici@6.25.0", "", {}, "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg=="], - "@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ALmj/53EXpcRqMbGpPJPP4UOSWw0q4VGpnDo7YctvsynjkrKDmoneDG/1a7VQnSPYHnJp6tTRMf5ZdxZ5whulg=="], @@ -4588,8 +4347,6 @@ "@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], "@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], @@ -4604,16 +4361,12 @@ "@antfu/install-pkg/package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], - "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1032.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-n+PU8Z+gll7p3wDrH+Wo6fkt8sPrVnq30YYM6Ryga95oJlEneNMEbDHj0iqjMX3V7gaGdJo/hJWyPo4lscP+mA=="], - "@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], - "@axe-core/playwright/axe-core": ["axe-core@4.11.4", "", {}, "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA=="], "@azure/identity/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], @@ -4628,12 +4381,8 @@ "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@bufbuild/protoplugin/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], - "@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], - "@changesets/changelog-github/dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="], - "@changesets/write/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], "@effect/platform-node/undici": ["undici@8.1.0", "", {}, "sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw=="], @@ -4660,7 +4409,7 @@ "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@hono/standard-validator/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], "@kilocode/kilo-docs/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -4674,12 +4423,6 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - "@modelcontextprotocol/sdk/@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], - - "@modelcontextprotocol/sdk/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], - - "@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "@morphllm/morphsdk/ai": ["ai@6.0.158", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="], "@morphllm/morphsdk/openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="], @@ -4698,9 +4441,9 @@ "@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], - "@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], + "@octokit/plugin-request-log/@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="], - "@octokit/plugin-retry/@octokit/types": ["@octokit/types@6.41.0", "", { "dependencies": { "@octokit/openapi-types": "^12.11.0" } }, "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg=="], + "@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], "@octokit/request/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], @@ -4712,14 +4455,8 @@ "@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="], - "@octokit/rest/@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="], - "@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="], - "@openauthjs/openauth/@standard-schema/spec": ["@standard-schema/spec@1.0.0-beta.3", "", {}, "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw=="], - - "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], - "@opencode-ai/core/@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.993.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.993.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-cognito-identity": "^3.972.3", "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-login": "^3.972.9", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-1M/nukgPSLqe9krzOKHnE8OylUaKAiokAV3xRLdeExVHcRE7WG5uzCTKWTj1imKvPjDqXq/FWhlbbdWIn7xIwA=="], "@opencode-ai/storybook/@storybook/addon-a11y": ["@storybook/addon-a11y@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.3.5" } }, "sha512-5k6lpgfIeLxvNhE8v3wEzdiu73ONKjF4gmH1AHvfqYd8kIVzQJai0KCDxgvqNncXHQhIWkaf1fg6+9hKaYJyaw=="], @@ -4750,21 +4487,11 @@ "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - "@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], - - "@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="], - - "@protobuf-ts/plugin/typescript": ["typescript@3.9.10", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="], - "@qdrant/js-client-rest/undici": ["undici@6.25.0", "", {}, "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg=="], - "@shikijs/engine-javascript/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], + "@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.9.2", "", { "dependencies": { "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3q/mzmw09B2B6PgFNeiaN8pkNOixWS726IHmJEpjDAcneDPMQmUg2cweT9cWXY4XcyQS3i6mOOUgQz9RRUP6HA=="], - "@shikijs/engine-oniguruma/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], - - "@shikijs/langs/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], - - "@shikijs/themes/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], + "@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.9.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw=="], "@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="], @@ -4810,8 +4537,6 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - "@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@vscode/ripgrep/yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], "@vscode/test-cli/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], @@ -4848,10 +4573,6 @@ "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], - "archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - - "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - "ava/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "ava/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], @@ -4866,22 +4587,12 @@ "ava/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - "aws-sdk/events": ["events@1.1.1", "", {}, "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw=="], - - "aws-sdk/uuid": ["uuid@8.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw=="], - - "aws-sdk/xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], - "axios/proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], "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=="], - "bl/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - - "buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], @@ -4906,8 +4617,6 @@ "command-line-usage/typical": ["typical@7.3.0", "", {}, "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw=="], - "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], @@ -4926,8 +4635,6 @@ "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "effect/uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], "encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], @@ -4938,6 +4645,8 @@ "enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "esbuild-plugin-solid/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], + "esbuild-plugin-solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="], "eslint/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], @@ -5002,8 +4711,6 @@ "knip/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "light-my-request/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], @@ -5052,20 +4759,6 @@ "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], - "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], - - "opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="], - - "opencontrol/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="], - - "opencontrol/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], - - "opencontrol/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], - - "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], - - "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "opentui-spinner/@opentui/core": ["@opentui/core@0.2.10", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.10", "@opentui/core-darwin-x64": "0.2.10", "@opentui/core-linux-arm64": "0.2.10", "@opentui/core-linux-x64": "0.2.10", "@opentui/core-win32-arm64": "0.2.10", "@opentui/core-win32-x64": "0.2.10" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-oviCtx0jYjc7F8X2b8+0IkQLg6WH47Nwl6CFeZo5dU0k6OpSbTbi07ZleObaiECAp+S1YLhAtVdgzHU7hBZlaw=="], "opentui-spinner/@opentui/solid": ["@opentui/solid@0.2.10", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.10", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-+4/MB90yIQiPwg8Y4wY092yva9BvRTsJeeeEO3e2H7P8k8zxYk4G9bzuhqYLxA9mTVQ+zVDlrmFoPQhT7vpIRw=="], @@ -5112,10 +4805,6 @@ "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], - "readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - - "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], - "redent/indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], @@ -5126,18 +4815,12 @@ "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "shiki/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], - - "shiki/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], - "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], "spdx-correct/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], - "sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="], - "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], "storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], @@ -5164,11 +4847,11 @@ "table-layout/array-back": ["array-back@6.2.3", "", {}, "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw=="], - "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "tar-stream/bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], @@ -5180,8 +4863,6 @@ "tree-sitter-bash/node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="], - "url/punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="], - "validate-npm-package-license/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], "venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], @@ -5190,6 +4871,8 @@ "vite-plugin-icons-spritesheet/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + "vite-plugin-solid/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], + "vite-plugin-solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="], "vitest/why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], @@ -5202,50 +4885,16 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "xml2js/sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], - "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], "yargs-unparser/decamelize": ["decamelize@4.0.0", "", {}, "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ=="], - "yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], - - "yazl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], - - "zod-to-json-schema/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "@actions/artifact/@actions/core/@actions/exec": ["@actions/exec@2.0.0", "", { "dependencies": { "@actions/io": "^2.0.0" } }, "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw=="], - - "@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/cerebras/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], - "@ai-sdk/cohere/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/openai-compatible/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], - "@ai-sdk/perplexity/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/togetherai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/vercel/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/xai/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], - "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], @@ -5278,9 +4927,19 @@ "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], - "@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], + "@octokit/plugin-request-log/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], - "@octokit/plugin-retry/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@12.11.0", "", {}, "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ=="], + "@octokit/plugin-request-log/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], + + "@octokit/plugin-request-log/@octokit/core/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], + + "@octokit/plugin-request-log/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + + "@octokit/plugin-request-log/@octokit/core/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@octokit/plugin-request-log/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], "@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -5322,8 +4981,6 @@ "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@storybook/addon-links/storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], "@storybook/addon-links/storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], @@ -5382,8 +5039,6 @@ "@textlint/linter-formatter/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@vscode/ripgrep/yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], - "@vscode/test-cli/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@vscode/test-cli/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -5426,12 +5081,6 @@ "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "archiver-utils/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - - "archiver-utils/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - - "archiver-utils/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "ava/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "ava/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -5446,18 +5095,12 @@ "ava/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "aws-sdk/xml2js/sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], - - "aws-sdk/xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - "babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], "babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], "babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "c8/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], @@ -5528,6 +5171,8 @@ "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "esbuild-plugin-solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -5564,12 +5209,12 @@ "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - - "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "mocha/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "mocha/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], @@ -5586,14 +5231,6 @@ "normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "opencontrol/@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], - - "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], - - "opencontrol/@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+lbDDj42Og+UtTZEwlHhGXichmOlkxSqn0J+Jqjat5/Tt5oZykj1NZjFIQ7ZSz4Miz7EmZwgYKE2CyOmmm9MoQ=="], "opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-5iAoA0aqMWWAQ93nh8Bb0ipwt9h+tvEFc88+YO9St43uUJ+XrXcmMj3T8wtl6dSu/SN0UoDWNaUMHUmtykiPtg=="], @@ -5606,8 +5243,6 @@ "opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.10", "", { "os": "win32", "cpu": "x64" }, "sha512-Ki+qNBlIFW5K2wcG/RHrlPp7yEQKXeiNX3mlje25iwX62Ac5w391HBpOmUjbPoq20McPyDRnhbLfbXQSPtickg=="], - "opentui-spinner/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], - "ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -5630,10 +5265,6 @@ "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], - "readable-stream/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], - "secretlint/globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], "storybook/@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], @@ -5648,9 +5279,7 @@ "table/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "tar-fs/tar-stream/bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - - "tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "tar-stream/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "test-exclude/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], @@ -5658,14 +5287,12 @@ "test-exclude/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "vite-plugin-solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@actions/artifact/@actions/core/@actions/exec/@actions/io": ["@actions/io@2.0.0", "", {}, "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg=="], - - "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], @@ -5680,6 +5307,10 @@ "@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@octokit/plugin-request-log/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + + "@octokit/plugin-request-log/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], @@ -5766,24 +5397,10 @@ "@vscode/test-cli/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@vscode/vsce/hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "@vscode/vsce/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "ai-gateway-provider/@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - - "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], - - "archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "ava/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "ava/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -5838,8 +5455,6 @@ "mocha/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "opentui-spinner/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "pkg-conf/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], @@ -5858,12 +5473,8 @@ "qrcode/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "storybook/@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - "tar-fs/tar-stream/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "test-exclude/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "test-exclude/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], @@ -5892,12 +5503,6 @@ "@vscode/test-cli/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "archiver-utils/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - - "archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "ava/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ava/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -5932,8 +5537,6 @@ "qrcode/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "tar-fs/tar-stream/bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "test-exclude/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "test-exclude/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], @@ -5944,10 +5547,6 @@ "@vscode/test-cli/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "archiver-utils/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "mocha/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "mocha/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], diff --git a/nix/hashes.json b/nix/hashes.json index 977af0237a2..49ae8cbc98c 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-Hd0VEzglgXhfHyK/8aSnR7VxE4x57ykNGWndASJRF3c=", - "aarch64-linux": "sha256-qXu4K8hQF/JqLzYGKphpob0e1I+Q8czJZ7RY8LcRTkU=", - "aarch64-darwin": "sha256-p+WYvQLgj7X1bT7LHTmu3HZ3W9uo1rz9wSoVOgrC5k8=", - "x86_64-darwin": "sha256-89Gy8mdutYBJcY+E57x2j3Rd0RRqrcaJstaQqex/nJA=" + "x86_64-linux": "sha256-qzU+msL1HicrkrYHNCitcutUqTFUatMArEuGgf/gOoE=", + "aarch64-linux": "sha256-GebxO5jgwnsNbvBdrbOlJ6u9C3/r6J7Q8WbzBv+wmhI=", + "aarch64-darwin": "sha256-NtAQ6QiL+iGaDxuFyvBiMFvBoEgBb5pK8b8xgkRdKx0=", + "x86_64-darwin": "sha256-rfxQmcEFsPD7AzSCBUZNxkNQUnkPnoN8a4shieEK1+Q=" } } diff --git a/package.json b/package.json index 9317d05d31d..477bd4d7ec7 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "@types/bun": "1.3.14", "@types/cross-spawn": "6.0.6", "@octokit/rest": "22.0.0", - "@hono/zod-validator": "0.4.2", "@opentui/core": "0.2.11", "@opentui/solid": "0.2.11", "ulid": "3.0.1", @@ -82,7 +81,6 @@ } }, "devDependencies": { - "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", "@types/mime-types": "3.0.1", "@typescript/native-preview": "catalog:", @@ -92,20 +90,16 @@ "oxlint-tsgolint": "0.21.0", "prettier": "3.6.2", "semver": "^7.6.0", - "sst": "3.18.10", "turbo": "2.9.14", "@types/bun": "catalog:", "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.27.10" }, "dependencies": { - "@aws-sdk/client-s3": "3.1025.0", "@kilocode/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@kilocode/sdk": "workspace:*", - "heap-snapshot-toolkit": "1.1.3", - "typescript": "catalog:", - "@morphllm/morphsdk": "0.2.166" + "typescript": "catalog:" }, "repository": { "type": "git", diff --git a/packages/containers/README.md b/packages/containers/README.md index 2efd38d7163..b67dcbb2d80 100644 --- a/packages/containers/README.md +++ b/packages/containers/README.md @@ -8,6 +8,7 @@ Images - `base`: Ubuntu 24.04 with common build tools and utilities - `bun-node`: `base` plus Bun and Node.js 24 +- `jetbrains`: `bun-node` plus Java 21 and the pre-cached Gradle distribution - `rust`: `bun-node` plus Rust (stable, minimal profile) - `tauri-linux`: `rust` plus Tauri Linux build dependencies - `publish`: `bun-node` plus Docker CLI and AUR tooling diff --git a/packages/containers/jetbrains/Dockerfile b/packages/containers/jetbrains/Dockerfile new file mode 100644 index 00000000000..707c5be73ca --- /dev/null +++ b/packages/containers/jetbrains/Dockerfile @@ -0,0 +1,35 @@ +# kilocode_change start +ARG REGISTRY=ghcr.io/kilo-org +FROM ${REGISTRY}/build/bun-node:24.04 + +ARG DEBIAN_FRONTEND=noninteractive +ARG GRADLE_VERSION=9.4.1 + +# Install Java 21 (apt handles multi-arch automatically) +RUN apt-get update \ + && apt-get install -y --no-install-recommends openjdk-21-jdk-headless \ + && rm -rf /var/lib/apt/lists/* + +# Create a stable /usr/lib/jvm/java-21 symlink that works on amd64 and arm64 +RUN java_home="$(dirname "$(dirname "$(readlink -f "$(which java)")")")" \ + && ln -nfs "${java_home}" /usr/lib/jvm/java-21 + +ENV JAVA_HOME=/usr/lib/jvm/java-21 + +# Pre-download the exact Gradle distribution used by the wrapper so CI jobs +# skip the download entirely. GRADLE_USER_HOME is set to a location that any +# user (including the GitHub Actions runner) can read. +ENV GRADLE_USER_HOME=/gradle-home + +RUN set -euo pipefail; \ + url="https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip"; \ + hash=$(echo -n "${url}" | md5sum | cut -d' ' -f1); \ + dir="/gradle-home/wrapper/dists/gradle-${GRADLE_VERSION}-bin/${hash}"; \ + mkdir -p "${dir}"; \ + curl -fsSL "${url}" -o "${dir}/gradle-${GRADLE_VERSION}-bin.zip"; \ + unzip -q "${dir}/gradle-${GRADLE_VERSION}-bin.zip" -d "${dir}"; \ + touch "${dir}/gradle-${GRADLE_VERSION}-bin.zip.ok"; \ + rm "${dir}/gradle-${GRADLE_VERSION}-bin.zip"; \ + java -version; \ + "${dir}/gradle-${GRADLE_VERSION}/bin/gradle" --version +# kilocode_change end diff --git a/packages/containers/script/build.ts b/packages/containers/script/build.ts index 82df81eb6ce..ff6054b5dcf 100644 --- a/packages/containers/script/build.ts +++ b/packages/containers/script/build.ts @@ -17,7 +17,7 @@ const manager = pkg.packageManager ?? "" const bun = manager.startsWith("bun@") ? manager.slice(4) : "" if (!bun) throw new Error("packageManager must be bun@") -const images = ["base", "bun-node", "rust", "tauri-linux", "publish"] +const images = ["base", "bun-node", "jetbrains", "rust", "tauri-linux", "publish"] // kilocode_change const setup = async () => { if (!push) return diff --git a/packages/core/package.json b/packages/core/package.json index 9b2c68a6f6c..8b327350796 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -22,8 +22,7 @@ "@types/bun": "catalog:", "@types/cross-spawn": "catalog:", "@types/npm-package-arg": "6.1.4", - "@types/npmcli__arborist": "6.3.3", - "@types/semver": "catalog:" + "@types/npmcli__arborist": "6.3.3" }, "dependencies": { "@kilocode/kilo-gateway": "workspace:*", diff --git a/packages/core/sst-env.d.ts b/packages/core/sst-env.d.ts deleted file mode 100644 index f25b9714550..00000000000 --- a/packages/core/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} diff --git a/packages/http-recorder/sst-env.d.ts b/packages/http-recorder/sst-env.d.ts deleted file mode 100644 index f25b9714550..00000000000 --- a/packages/http-recorder/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} diff --git a/packages/kilo-console/package.json b/packages/kilo-console/package.json old mode 100644 new mode 100755 diff --git a/packages/kilo-console/src/routes/config/CliNotificationsRoute.tsx b/packages/kilo-console/src/routes/config/CliNotificationsRoute.tsx index e8621cffc9a..84b48020eba 100644 --- a/packages/kilo-console/src/routes/config/CliNotificationsRoute.tsx +++ b/packages/kilo-console/src/routes/config/CliNotificationsRoute.tsx @@ -1,7 +1,14 @@ import { Button } from "@kilocode/kilo-web-ui/button" import { Card } from "@kilocode/kilo-web-ui/card" +import { CustomSelect, type SelectOption } from "../../components/CustomSelect" import { ConfigPage, ConfigTag as Tag } from "./ConfigPage" -import { useTuiNotificationSettings } from "./state/ui" +import { type TitleIcon, useTuiNotificationSettings } from "./state/ui" + +const icons = [ + { value: "none", label: "None" }, + { value: "unicode", label: "Unicode" }, + { value: "emojis", label: "Emojis" }, +] satisfies SelectOption[] function Toggle(props: { label: string @@ -34,57 +41,82 @@ export function CliNotificationsRoute() { return ( Save } > - -
-
-

Attention

-

Control when the TUI asks for attention and how it notifies you.

+
+ +
+
+

Terminal title

+

Choose how session status appears in the terminal tab title.

+
+
+
+
+ Title Icon + + None hides status icons. Unicode and Emojis show working, attention, and finished states. +
-
-
- state.setAlert(!state.alert())} - /> - state.setNotify(!state.notify())} - /> - state.setSound(!state.sound())} - /> -
+
+
) } diff --git a/packages/kilo-console/src/routes/config/state/ui.ts b/packages/kilo-console/src/routes/config/state/ui.ts index fa70ff062a0..7f94285bf86 100644 --- a/packages/kilo-console/src/routes/config/state/ui.ts +++ b/packages/kilo-console/src/routes/config/state/ui.ts @@ -10,6 +10,7 @@ export type Theme = { } type Diff = "auto" | "stacked" +export type TitleIcon = NonNullable const fallback = ["#0c0a09", "#fafaf9", "#f9f76f", "#a6a09b", "#3794ff", "#44403b"] @@ -204,15 +205,18 @@ export function useTuiNotificationSettings() { const [notify, setNotify] = createSignal(true) const [sound, setSound] = createSignal(true) const [volume, setVolume] = createSignal("0.4") + const [icon, setIcon] = createSignal("none") const [dirty, setDirty] = createSignal(false) createEffect(() => { if (dirty()) return - const cfg = ctx.data()?.tui.attention + const tui = ctx.data()?.tui + const cfg = tui?.attention setAlert(bool(cfg?.enabled, false)) setNotify(bool(cfg?.notifications, true)) setSound(bool(cfg?.sound, true)) setVolume(String(cfg?.volume ?? 0.4)) + setIcon(tui?.title_icon ?? "none") }) function change(run: () => void) { @@ -228,6 +232,7 @@ export function useTuiNotificationSettings() { } ctx.tui({ + title_icon: icon(), attention: { enabled: alert(), notifications: notify(), @@ -248,6 +253,8 @@ export function useTuiNotificationSettings() { setSound: (value: boolean) => change(() => setSound(value)), volume, setVolume: (value: string) => change(() => setVolume(value)), + icon, + setIcon: (value: TitleIcon) => change(() => setIcon(value)), dirty, save, } diff --git a/packages/kilo-console/src/styles/cli-ui.css b/packages/kilo-console/src/styles/cli-ui.css index 89c066f8036..c32380c9b44 100644 --- a/packages/kilo-console/src/styles/cli-ui.css +++ b/packages/kilo-console/src/styles/cli-ui.css @@ -8,17 +8,20 @@ background: var(--card); } -.kilo-console .ui-card:has(.console-diff-select[open]) { +.kilo-console .ui-card:has(.console-diff-select[open]), +.kilo-console .ui-card:has(.title-icon-select[open]) { position: relative; z-index: 40; overflow: visible; } -.kilo-console .console-diff-select[open] { +.kilo-console .console-diff-select[open], +.kilo-console .title-icon-select[open] { z-index: 41; } -.kilo-console .console-diff-select .models-select-menu { +.kilo-console .console-diff-select .models-select-menu, +.kilo-console .title-icon-select .models-select-menu { z-index: 42; } diff --git a/packages/kilo-docs/lib/nav/customize.ts b/packages/kilo-docs/lib/nav/customize.ts index 83006b9e1bd..a3bd302034b 100644 --- a/packages/kilo-docs/lib/nav/customize.ts +++ b/packages/kilo-docs/lib/nav/customize.ts @@ -22,6 +22,11 @@ export const CustomizeNav: NavSection[] = [ children: "Custom Subagents", platform: "new", }, + { + href: "/customize/agent-permissions", + children: "Agent Permissions", + platform: "new", + }, { href: "/customize/agents-md", children: "agents.md" }, { href: "/customize/workflows", children: "Workflows", platform: "new" }, { href: "/customize/skills", children: "Skills" }, diff --git a/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md b/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md index 0acb8e81f21..710f38bfee8 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md @@ -142,24 +142,40 @@ The model selection is remembered per mode across sessions. For details on configuring subagent models, see [Custom Subagents](/docs/customize/custom-subagents). -## Selecting a Model via a Link (VS Code) +## Selecting a Model or Agent via a Link (VS Code) -The VS Code extension supports a `vscode://` protocol handler that lets you open VS Code and automatically select a specific model — no manual picker interaction required. This is useful for sharing model recommendations, launching a specific model tier from a web page, or switching quickly to a newly announced model. +The VS Code extension supports a `vscode://` protocol handler that lets you open VS Code and automatically select a model, an agent, or both — no manual picker interaction required. This is useful for sharing model recommendations, launching a specific model tier from a web page, or switching quickly to a preferred agent. ### URL Format +Include at least one of the `model` or `agent` parameters: + ``` -vscode://kilocode.kilo-code/kilocode/model?model= +vscode://kilocode.kilo-code/kilocode/switch?model= +vscode://kilocode.kilo-code/kilocode/switch?agent= +vscode://kilocode.kilo-code/kilocode/switch?model=&agent= ``` -Replace `` with the Kilo Gateway model ID you want to select (e.g. `kilo-auto/free`). Only models available in the Kilo Gateway catalog are accepted; the link is silently ignored if the model ID is not found. +Replace `` with a Kilo Gateway model ID such as `kilo-auto/free`. Replace `` with a visible primary agent ID such as `code` or `plan`, rather than its display name. ### Example: Auto Free To open Kilo Code and switch to the [Auto Free](/docs/code-with-ai/agents/auto-model) tier (`kilo-auto/free`), use: ``` -vscode://kilocode.kilo-code/kilocode/model?model=kilo-auto%2Ffree +vscode://kilocode.kilo-code/kilocode/switch?model=kilo-auto%2Ffree +``` + +To switch only to Plan and use its normal model selection, specify the agent without a model: + +``` +vscode://kilocode.kilo-code/kilocode/switch?agent=plan +``` + +To select both at the same time, include both parameters: + +``` +vscode://kilocode.kilo-code/kilocode/switch?model=kilo-auto%2Ffree&agent=plan ``` {% callout type="tip" %} @@ -168,18 +184,20 @@ URL-encode the `/` in model IDs as `%2F` when embedding this URL in HTML links o ### How It Works -- **VS Code open**: the Kilo sidebar is focused and the model is selected in the active session immediately. -- **VS Code closed**: VS Code launches, then applies the model selection once the extension is ready. -- The model is validated against the current Kilo Gateway model catalog before being applied. If the model ID is not found in the catalog, the deep link is silently ignored. -- Model selection via deep link follows the same precedence as using the model picker: it applies as a session override for the active session (or sets the next-session model when no session is active), and updates your model recents list. It does **not** change your configured default model in settings. +- **VS Code open**: the Kilo sidebar is focused and the linked selection is applied to the active session immediately. +- **VS Code closed**: VS Code launches, then applies the selection once the extension is ready. +- When `model` is provided, it must identify a model in the current Kilo Gateway catalog. Invalid or unavailable models cause the deep link to be ignored. +- When `agent` is provided, it must identify a visible primary agent. Invalid or unavailable agents cause the deep link to be ignored. +- An agent-only link uses the model that would normally be selected for that agent. When both parameters are present, the agent is selected first so the linked model applies to it. +- The selection follows the same precedence as using the pickers: it updates the active session, or the next session when no session is active. It does **not** change your configured defaults in settings. ### Sharing and Embedding You can embed these links in a web page: ```html - - Open Kilo Code with Auto Free + + Open Kilo Code with Auto Free in Plan ``` diff --git a/packages/kilo-docs/pages/code-with-ai/features/speech-to-text.md b/packages/kilo-docs/pages/code-with-ai/features/speech-to-text.md index c98ae767c66..f95f55bfadb 100644 --- a/packages/kilo-docs/pages/code-with-ai/features/speech-to-text.md +++ b/packages/kilo-docs/pages/code-with-ai/features/speech-to-text.md @@ -41,7 +41,7 @@ Enable and sign in to the Kilo provider to use voice input in prompt fields. Req ## Choose a model -You can optionally choose a transcription model in **Settings** > **Experimental** > **Speech to Text Model**. Kilo stores this choice as `experimental.speech_to_text_model` in your global Kilo CLI config (`~/.config/kilo/kilo.jsonc`). +You can optionally choose a transcription model in **Settings** > **Models** > **Speech to Text Model**. Kilo stores this choice as `experimental.speech_to_text_model` in your global Kilo CLI config (`~/.config/kilo/kilo.jsonc`). --- diff --git a/packages/kilo-docs/pages/customize/agent-permissions.md b/packages/kilo-docs/pages/customize/agent-permissions.md new file mode 100644 index 00000000000..c0764c27829 --- /dev/null +++ b/packages/kilo-docs/pages/customize/agent-permissions.md @@ -0,0 +1,189 @@ +--- +title: "Agent Permissions" +description: "Configure Kilo Code agent permission rules for tools, shell commands, files, and subagents" +platform: new +--- + +# Agent Permissions + +Agent permissions decide whether a tool call is allowed, asks for approval, or is denied. + +This page focuses on Markdown agent files, where permission rules are written as YAML frontmatter under the `permission` key. For global defaults in `kilo.jsonc`, use the JSON examples in [Auto-Approving Actions](/docs/getting-started/settings/auto-approving-actions#glob-pattern-rules). + +## Actions + +Each permission rule uses one of these actions: + +| Action | Behavior | +|---|---| +| `allow` | Run the matching tool call without asking. | +| `ask` | Prompt before running the matching tool call. | +| `deny` | Block the matching tool call. | + +You can write each permission as one action for the whole tool or as a pattern map: + +```yaml +permission: + read: allow + edit: + "*": deny + "*.md": allow + bash: + "*": ask + "git status *": allow +``` + +## Rule Precedence + +Permission rules are evaluated in config order. When more than one rule matches the requested permission and target pattern, the last matching rule wins. + +Put broad fallbacks first and exceptions after them: + +```yaml +permission: + bash: + "*": ask + "uv *": allow +``` + +With that config, `uv pip install ...` is allowed because `uv *` appears after the catch-all `*`. + +If you put the catch-all last, it overrides the earlier specific rule: + +```yaml +permission: + bash: + "uv *": allow + "*": ask +``` + +With that config, `uv pip install ...` asks because the later `*` rule also matches. + +Top-level permission keys follow the same rule. For example, this lets `bash` override the global fallback: + +```yaml +permission: + "*": ask + bash: allow +``` + +This does the opposite because the top-level `*` is later: + +```yaml +permission: + bash: allow + "*": ask +``` + +## Patterns + +Permission patterns use glob matching: + +| Pattern | Matches | +|---|---| +| `*` | Any target for that permission. | +| `git *` | `git`, `git status`, `git log --oneline`, and other `git` commands. | +| `git status *` | `git status` with or without extra arguments. | +| `src/*` | Paths under `src/`. | +| `*.env` | Files ending in `.env`, including nested paths such as `apps/web/.env`. | + +The matcher normalizes Windows backslashes to forward slashes before matching. On Windows, matching is case-insensitive; on Unix-like systems, matching is case-sensitive. Prefer forward slashes in config because they work across platforms. + +`~`, `~/...`, `$HOME`, and `$HOME/...` at the start of a pattern are expanded to your home directory when the config is loaded. + +## File Paths + +File tools such as `read`, `edit`, and `write` resolve the input path first, then check permissions against the path relative to the current worktree. + +For project files, use workspace-relative patterns: + +```yaml +permission: + read: + "*": ask + "docs/*": allow + "src/generated/*": deny + edit: + "*": deny + "*.md": allow +``` + +Absolute paths are mainly relevant for `external_directory` permissions and shell commands that touch paths outside the worktree. + +## Shell Commands + +The `bash` permission is checked against parsed shell command patterns. If a shell block contains multiple parsed commands, each relevant command must be permitted. A single denied command rejects the request. + +For example: + +```yaml +permission: + bash: + "*": ask + "cd *": allow + "git *": deny +``` + +For this command: + +```bash +cd "/project"; git status +``` + +Kilo checks the parsed command patterns. The `git status` command matches `git *`, so the request is denied. Directory changes and commands that access paths outside the worktree can also trigger `external_directory` checks. + +Built-in read-only agents include additional shell restrictions for write-like patterns such as output redirection, command substitution, pipes, and command chains. If you create your own read-only agent, prefer an explicit deny fallback and allow only the commands you trust: + +```yaml +permission: + bash: + "*": deny + "cat *": allow + "grep *": allow + "git status *": allow + "git diff *": allow +``` + +## Sensitive Files + +Kilo treats `.env` and `.env.*` reads as sensitive. Broad read approvals, such as `read: allow`, `read: { "*": allow }`, saved wildcard approvals, or allow-everything mode do not bypass the built-in prompt for these files. `.env.example` is treated as safe documentation and can be allowed by default. + +Use explicit sensitive-file rules only when you intentionally want that behavior for a specific agent: + +```yaml +permission: + read: + "*": allow + "*.env": ask + "*.env.*": ask + "*.env.example": allow +``` + +## Subagent Delegation + +Use `task` permission rules to control which subagents another agent may invoke: + +```yaml +permission: + task: + "*": deny + "code-reviewer": allow + "docs-writer": allow +``` + +This allows delegation only to `code-reviewer` and `docs-writer`. + +## Troubleshooting + +- If a specific rule appears to be ignored, check whether a later catch-all also matches. +- If a broad allow does not apply to `.env`, this is expected sensitive-file protection. +- If a shell command asks unexpectedly, check whether it was parsed into more than one command pattern or triggered `external_directory`. +- If path rules behave differently across operating systems, write patterns with forward slashes and workspace-relative paths where possible. + +## Related + +- [Custom Modes](/docs/customize/custom-modes) +- [Custom Subagents](/docs/customize/custom-subagents) +- [Auto-Approving Actions](/docs/getting-started/settings/auto-approving-actions) +- [.kilocodeignore](/docs/customize/context/kilocodeignore) +- [Tool Use Overview](/docs/automate/tools) diff --git a/packages/kilo-docs/pages/customize/custom-subagents.md b/packages/kilo-docs/pages/customize/custom-subagents.md index c1164c6701b..ad2b341b43e 100644 --- a/packages/kilo-docs/pages/customize/custom-subagents.md +++ b/packages/kilo-docs/pages/customize/custom-subagents.md @@ -191,7 +191,7 @@ The `permission` field controls what tools the subagent can use. Each tool permi } ``` -For bash commands, you can use glob patterns to set permissions per command. Rules are evaluated in order, with the **last matching rule winning**. +For bash commands, you can use glob patterns to set permissions per command. Rules are evaluated in order, with the **last matching rule winning**. See [Agent Permissions](/docs/customize/agent-permissions) for rule precedence, shell command patterns, path matching, and sensitive-file behavior. You can also control which subagents an agent can invoke via `permission.task`: diff --git a/packages/kilo-docs/pages/customize/index.md b/packages/kilo-docs/pages/customize/index.md index 2a4e54aeb3d..9413cb762ba 100644 --- a/packages/kilo-docs/pages/customize/index.md +++ b/packages/kilo-docs/pages/customize/index.md @@ -17,6 +17,7 @@ Configure how Kilo Code behaves and responds: - [**Custom Rules**](/docs/customize/custom-rules) - Define rules that apply to specific file types or situations - [**Custom Instructions**](/docs/customize/custom-instructions) - Add project-specific guidelines and context - [**Custom Subagents**](/docs/customize/custom-subagents) - Create specialized subagents with custom prompts, models, and permissions +- [**Agent Permissions**](/docs/customize/agent-permissions) - Configure rule precedence, shell command patterns, file paths, and subagent delegation - [**agents.md**](/docs/customize/agents-md) - Configure agent behavior at the project level - [**Workflows**](/docs/customize/workflows) - Automate multi-step processes - [**Skills**](/docs/customize/skills) - Extend Kilo's capabilities with reusable skill definitions diff --git a/packages/kilo-docs/pages/getting-started/settings/auto-approving-actions.md b/packages/kilo-docs/pages/getting-started/settings/auto-approving-actions.md index 324b7f058a8..f556be70ca6 100644 --- a/packages/kilo-docs/pages/getting-started/settings/auto-approving-actions.md +++ b/packages/kilo-docs/pages/getting-started/settings/auto-approving-actions.md @@ -125,6 +125,40 @@ Permissions are configured under the `permission` key in `kilo.jsonc`. The follo Instead of a simple `"allow"` or `"deny"`, each tool can use glob-pattern rules for granular control. Patterns are matched against the tool's arguments (command strings, file paths, etc.), and the last matching rule wins. +### Rule Precedence + +Permission rules are evaluated in config order. When more than one rule matches the requested permission and target pattern, the last matching rule wins. + +Put broad fallbacks first and exceptions after them: + +```json +{ + "permission": { + "bash": { + "*": "ask", + "uv *": "allow" + } + } +} +``` + +With that config, `uv pip install ...` is allowed because `uv *` appears after the catch-all `*`. + +If you put the catch-all last, it overrides the earlier specific rule: + +```json +{ + "permission": { + "bash": { + "uv *": "allow", + "*": "ask" + } + } +} +``` + +With that config, `uv pip install ...` asks because the later `*` rule also matches. + ### Example: Shell Commands Allow git commands automatically, but prompt for everything else: @@ -133,8 +167,8 @@ Allow git commands automatically, but prompt for everything else: { "permission": { "bash": { - "git *": "allow", - "*": "ask" + "*": "ask", + "git *": "allow" } } } @@ -148,8 +182,8 @@ Prompt before reading `.env` files, but allow all other reads: { "permission": { "read": { - "*.env": "ask", - "*": "allow" + "*": "allow", + "*.env": "ask" } } } @@ -163,11 +197,11 @@ Deny `rm -rf` commands, allow common dev commands, and ask for anything else: { "permission": { "bash": { + "*": "ask", "rm -rf *": "deny", "npm *": "allow", "bun *": "allow", - "git *": "allow", - "*": "ask" + "git *": "allow" } } } @@ -185,7 +219,7 @@ Different agents can have different permission levels. Override the default perm "agent": { "code": { "permission": { - "bash": { "git *": "allow", "*": "ask" } + "bash": { "*": "ask", "git *": "allow" } } }, "plan": { @@ -199,6 +233,26 @@ Different agents can have different permission levels. Override the default perm In this example, the `code` agent can run `git` commands automatically and asks for other shell commands, while the `plan` agent cannot run shell commands at all. +## Markdown Agent Files + +If you define agents in Markdown files, the `permission` frontmatter uses the same `allow` / `ask` / `deny` values and glob patterns as `kilo.jsonc`: + +```markdown +--- +description: Reviews code for quality and best practices +mode: subagent +permission: + bash: + "*": ask + "git *": allow + read: + "*": allow + "*.env": ask +--- +``` + +This is the same permission shape described in [Agent Permissions](/docs/customize/agent-permissions), just shown in the agent-file format. + ## Runtime Permission Requests When a tool is set to `"ask"`, Kilo pauses and displays a permission prompt. You have three options: @@ -234,12 +288,12 @@ This is a custom example showing the available configuration options — it does ```json { "permission": { - "read": { "*.env": "ask", "*": "allow" }, - "edit": { "*.env": "ask", "*": "allow" }, + "read": { "*": "allow", "*.env": "ask" }, + "edit": { "*": "allow", "*.env": "ask" }, "glob": { "*": "allow" }, "grep": { "*": "allow" }, "list": { "*": "allow" }, - "bash": { "git *": "allow", "npm *": "allow", "*": "ask" }, + "bash": { "*": "ask", "git *": "allow", "npm *": "allow" }, "task": { "*": "allow" }, "skill": { "*": "allow" }, "lsp": { "*": "allow" }, @@ -253,7 +307,7 @@ This is a custom example showing the available configuration options — it does "agent": { "code": { "permission": { - "bash": { "git *": "allow", "npm *": "allow", "*": "ask" } + "bash": { "*": "ask", "git *": "allow", "npm *": "allow" } } } } diff --git a/packages/kilo-docs/pages/getting-started/settings/index.md b/packages/kilo-docs/pages/getting-started/settings/index.md index 79faf946cf5..a3e9ddd5361 100644 --- a/packages/kilo-docs/pages/getting-started/settings/index.md +++ b/packages/kilo-docs/pages/getting-started/settings/index.md @@ -47,6 +47,18 @@ Use **Local Config** or **Global Config** in the Settings header to open the mat If you check config files into version control, make sure they do not contain API keys or other secrets (e.g., `provider.*.options.apiKey`). Use environment variables for credentials instead. {% /callout %} +### Voice Transcription Model + +When the Kilo provider is enabled and you are signed in, choose the transcription model under **Models** > **Speech to Text Model**. This stores `experimental.speech_to_text_model` in your global Kilo CLI config: + +```json +{ + "experimental": { + "speech_to_text_model": "openai/whisper-large-v3-turbo" + } +} +``` + ### Reasoning Blocks Reasoning blocks stay expanded by default in the VS Code chat UI. Enable **Auto-Collapse Reasoning** in the Display tab, or set `auto_collapse_reasoning` in `kilo.jsonc`, to collapse them after the agent finishes writing them: @@ -198,20 +210,9 @@ Available experimental settings include: - **Share mode** - `manual`, `auto`, or `disabled` session sharing - **LSP integration** - expose language server diagnostics to the agent - **Paste summary** - summarize large clipboard pastes before including them -- **Speech to Text Model** - optionally select the transcription model - **Batch tool** - allow the agent to batch multiple tool calls in one step - **OpenTelemetry** - enable Kilo telemetry and optional OTLP export when configured -Voice input appears automatically when the Kilo provider is enabled and you are signed in. Choosing **Speech to Text Model** stores `experimental.speech_to_text_model` in your global Kilo CLI config (`~/.config/kilo/kilo.jsonc`): - -```json -{ - "experimental": { - "speech_to_text_model": "openai/gpt-4o-mini-transcribe" - } -} -``` - Advanced options not exposed in the UI can be configured via the `experimental` key in `kilo.jsonc`: ```json diff --git a/packages/kilo-docs/pages/kiloclaw/development-tools/composio.md b/packages/kilo-docs/pages/kiloclaw/development-tools/composio.md index e3ac585a0dc..08ac935bc19 100644 --- a/packages/kilo-docs/pages/kiloclaw/development-tools/composio.md +++ b/packages/kilo-docs/pages/kiloclaw/development-tools/composio.md @@ -8,7 +8,7 @@ description: "Connect Composio to your KiloClaw agent to access hundreds of tool Connect Composio to your KiloClaw agent to instantly unlock access to 250+ tool integrations — from Salesforce and HubSpot to Notion, Jira, and beyond. Composio is a platform that handles the authentication and connection details for each service, so your agent can use them without you having to set up each one individually. {% callout type="info" title="Tip" %} -Browse the full list of tools Composio supports at [composio.dev/tools](https://composio.dev/tools). If a tool you need is listed there, you can connect it to KiloClaw through Composio in minutes. +Browse the full list of toolkits Composio supports at [composio.dev/toolkits](https://composio.dev/toolkits). If a toolkit you need is listed there, you can connect it to KiloClaw through Composio in minutes. {% /callout %} ## Prerequisites diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/sidebar-search-open-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/sidebar-search-open-chromium-linux.png new file mode 100644 index 00000000000..3a21dccf87c --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/sidebar-search-open-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29bb5361de5ac4b7acf247b8f9976c715c4e300448f6e504ec791566eca91219 +size 25750 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-multiple-tabs-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-multiple-tabs-chromium-linux.png index 6ab79b4689a..23c66f21c9f 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-multiple-tabs-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-multiple-tabs-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7ec87f1e046cf1a44688b952c0d994899119c6663bf627026688bf337b56127 -size 2411 +oid sha256:1370f2dcf5c50258971e0665ea151f2c522f566619745d58d6a10f3914949e6c +size 2521 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-single-tab-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-single-tab-chromium-linux.png index 905e28d6b85..8e3f4a2492e 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-single-tab-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-single-tab-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ee0856c65e445a230cf3e94bfe592c98306b7502e44a2602b5fac2f6d69937a -size 4601 +oid sha256:b8af37e0c169811092437b71110c63571ae43c4d35e12f6890be8295a3cb67da +size 4569 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-with-review-tab-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-with-review-tab-chromium-linux.png index 83fda13fbb4..3eb073991f9 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-with-review-tab-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-with-review-tab-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1ee2d880a055b03664d594598a6e8764ab27d443a578572fd01b37451cb0193 -size 3193 +oid sha256:16fdb6361199c58a8e99ad2661dc47791b0f4737ada16760151a26e7ebdd8bd1 +size 3044 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-apply-patch-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-apply-patch-chromium-linux.png index 795a6cd39a1..f38949019a1 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-apply-patch-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-apply-patch-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c1b7ab570623823e73afbbf2ab7d6b66ab16d948405ef9ced0a012441b70301 -size 25148 +oid sha256:e872bc8ad05ad2a7ad1dd6de8a2ff427957fd278cee46940db0b12d84fe42ecf +size 25534 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-edit-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-edit-chromium-linux.png index ca1605de57c..0eff7b16fa8 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-edit-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-edit-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce6ad833325a582ca6a2107c94f007c589816c149179fa9b317f060980e5e4fa -size 20250 +oid sha256:ff33e800f4d2379bcc997228a1e4dd83433521b931ae85edb7c8f0c4c93405e7 +size 20540 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-accessible-labels-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-accessible-labels-chromium-linux.png index db7441c87c1..34dae072479 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-accessible-labels-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-accessible-labels-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bae54b771e2d7da8c8b1dcab42d9d51662ec888ebb618b24af11e597c79df7b7 -size 37836 +oid sha256:9e041b27abbb19de884e1b928d415d49182872b06018411ed589197424a5ac22 +size 48674 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png index db7441c87c1..34dae072479 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bae54b771e2d7da8c8b1dcab42d9d51662ec888ebb618b24af11e597c79df7b7 -size 37836 +oid sha256:9e041b27abbb19de884e1b928d415d49182872b06018411ed589197424a5ac22 +size 48674 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-speech-to-text-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-speech-to-text-chromium-linux.png new file mode 100644 index 00000000000..866111072a1 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-speech-to-text-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ced53d4974eeacc629eede823c7e37191f40ea2816f80f18c1de1a833fbcc614 +size 45341 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/settings-panel-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/settings-panel-chromium-linux.png index 9fb41085b48..545366bac04 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/settings-panel-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/settings-panel-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57fa5c60dd92a2f4299e58ead8a682c4802de415a496c247af2eba2faf0d63e4 -size 50341 +oid sha256:f4dbd3be2cdf72a789c17076afa73ddeb2eacbe201ace8d8a2657eb95b23a413 +size 53851 diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index 5e37cf4e1f8..bcb24a6267a 100644 --- a/packages/kilo-gateway/package.json +++ b/packages/kilo-gateway/package.json @@ -33,7 +33,6 @@ }, "dependencies": { "@kilocode/plugin": "workspace:*", - "@kilocode/sdk": "workspace:*", "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/openai": "3.0.53", diff --git a/packages/kilo-indexing/src/file/ignore.ts b/packages/kilo-indexing/src/file/ignore.ts index 292405cb9f9..df5c45e889f 100644 --- a/packages/kilo-indexing/src/file/ignore.ts +++ b/packages/kilo-indexing/src/file/ignore.ts @@ -44,6 +44,8 @@ export namespace FileIgnore { "**/*.log", "**/coverage/**", "**/.nyc_output/**", + "**/.kilo/worktrees/**", + "**/.kilocode/worktrees/**", ] export const PATTERNS = [...files, ...folders] diff --git a/packages/kilo-indexing/src/indexing/cache-manager.ts b/packages/kilo-indexing/src/indexing/cache-manager.ts index 5973cf96c74..ba5cd3735f1 100644 --- a/packages/kilo-indexing/src/indexing/cache-manager.ts +++ b/packages/kilo-indexing/src/indexing/cache-manager.ts @@ -16,6 +16,7 @@ export class CacheManager implements ICacheManager { private readonly cachePath: string private fileHashes: Record = {} private saveTimer: ReturnType | undefined + private saveTask = Promise.resolve() constructor( private readonly cacheDirectory: string, @@ -36,28 +37,36 @@ export class CacheManager implements ICacheManager { private scheduleSave(): void { if (this.saveTimer) clearTimeout(this.saveTimer) - this.saveTimer = setTimeout(() => this.performSave(), 1500) + this.saveTimer = setTimeout(() => { + void this.flush().catch((err) => log.error("failed to save cache", { err })) + }, 1500) } private async performSave(): Promise { - try { - await fs.mkdir(path.dirname(this.cachePath), { recursive: true }) - const tmp = `${this.cachePath}.tmp` - await fs.writeFile(tmp, JSON.stringify(this.fileHashes), "utf-8") - await fs.rename(tmp, this.cachePath) - } catch (err) { + await fs.mkdir(path.dirname(this.cachePath), { recursive: true }) + const tmp = `${this.cachePath}.tmp` + await fs.writeFile(tmp, JSON.stringify(this.fileHashes), "utf-8") + await fs.rename(tmp, this.cachePath) + } + + async flush(): Promise { + if (this.saveTimer) clearTimeout(this.saveTimer) + this.saveTimer = undefined + const task = this.saveTask.then(() => this.performSave()) + this.saveTask = task.catch((err) => { log.error("failed to save cache", { err }) - } + }) + await task + } + + seedHashes(hashes: Readonly>): void { + this.fileHashes = { ...hashes } + this.scheduleSave() } async clearCacheFile(): Promise { - try { - this.fileHashes = {} - await fs.mkdir(path.dirname(this.cachePath), { recursive: true }) - await fs.writeFile(this.cachePath, "{}", "utf-8") - } catch (err) { - log.error("failed to clear cache file", { err }) - } + this.fileHashes = {} + await this.flush() } getHash(filePath: string): string | undefined { @@ -77,4 +86,16 @@ export class CacheManager implements ICacheManager { getAllHashes(): Record { return { ...this.fileHashes } } + + signature(): string { + const entries = Object.entries(this.fileHashes).sort(([left], [right]) => left.localeCompare(right)) + return createHash("sha256").update(JSON.stringify(entries)).digest("hex") + } + + async stamp(): Promise { + return fs + .stat(this.cachePath) + .then((value) => `${value.mtimeMs}:${value.ctimeMs}:${value.size}`) + .catch(() => undefined) + } } diff --git a/packages/kilo-indexing/src/indexing/interfaces/file-processor.ts b/packages/kilo-indexing/src/indexing/interfaces/file-processor.ts index 03a80a823c2..704d5761ee7 100644 --- a/packages/kilo-indexing/src/indexing/interfaces/file-processor.ts +++ b/packages/kilo-indexing/src/indexing/interfaces/file-processor.ts @@ -1,6 +1,7 @@ import type { PointStruct } from "./vector-store" import type { Disposable, Emitter } from "../runtime" import type { IndexingTelemetryMode } from "./telemetry" +import type { WorktreeOverlay } from "../worktree-overlay" export interface ICodeParser { parseFile( @@ -36,6 +37,8 @@ export interface IFileWatcher extends Disposable { initialize(): Promise updateBatchSegmentThreshold(newThreshold: number): void setCollecting(collecting: boolean): void + setOverlay?(overlay?: WorktreeOverlay): void + shutdown?(): Promise readonly onDidStartBatchProcessing: Emitter readonly onBatchProgressUpdate: Emitter<{ diff --git a/packages/kilo-indexing/src/indexing/interfaces/manager.ts b/packages/kilo-indexing/src/indexing/interfaces/manager.ts index 5d1e0433f49..c4ffad9efc0 100644 --- a/packages/kilo-indexing/src/indexing/interfaces/manager.ts +++ b/packages/kilo-indexing/src/indexing/interfaces/manager.ts @@ -31,7 +31,7 @@ export interface ICodeIndexManager { totalItems: number currentItemUnit: string } - dispose(): void + dispose(): Promise } export type IndexingState = "Standby" | "Indexing" | "Indexed" | "Error" diff --git a/packages/kilo-indexing/src/indexing/interfaces/vector-store.ts b/packages/kilo-indexing/src/indexing/interfaces/vector-store.ts index 43253e0d8d1..7712502f14a 100644 --- a/packages/kilo-indexing/src/indexing/interfaces/vector-store.ts +++ b/packages/kilo-indexing/src/indexing/interfaces/vector-store.ts @@ -8,6 +8,12 @@ export type PointStruct = { } export interface IVectorStore { + /** + * Opens an existing complete store without mutating it. + */ + openExisting?(): Promise + close?(): Promise + /** * Initializes the vector store * @returns Promise resolving to boolean indicating if a new collection was created @@ -90,6 +96,7 @@ export interface VectorStoreSearchResult { export interface Payload { filePath: string + fileHash?: string codeChunk: string startLine: number endLine: number diff --git a/packages/kilo-indexing/src/indexing/manager.ts b/packages/kilo-indexing/src/indexing/manager.ts index 3dbd8c3b428..05e4c5fc15d 100644 --- a/packages/kilo-indexing/src/indexing/manager.ts +++ b/packages/kilo-indexing/src/indexing/manager.ts @@ -1,4 +1,5 @@ -import type { VectorStoreSearchResult } from "./interfaces" +import path from "path" +import type { IVectorStore, VectorStoreSearchResult } from "./interfaces" import type { IndexingState } from "./interfaces/manager" import type { IndexingTelemetryEvent, IndexingTelemetryMeta, IndexingTelemetryTrigger } from "./interfaces/telemetry" import { CodeIndexConfigManager, type IndexingConfigInput } from "./config-manager" @@ -12,8 +13,18 @@ import { Emitter } from "./runtime" import { Log } from "../util/log" import { loadIgnore } from "./shared/load-ignore" import { sanitizeErrorMessage } from "./shared/validation-helpers" +import { WorktreeOverlay } from "./worktree-overlay" const log = Log.create({ service: "indexing-manager" }) +const BASELINE_CHECK_INTERVAL = 1_000 +const BASELINE_SIGNATURE_INTERVAL = 30_000 + +type Baseline = { + store?: IVectorStore + signature: string + stamp?: string + overlay?: WorktreeOverlay +} /** * RATIONALE: Removed the static singleton Map and vscode.ExtensionContext. @@ -29,6 +40,13 @@ export class CodeIndexManager { private _orchestrator: CodeIndexOrchestrator | undefined private _searchService: CodeIndexSearchService | undefined private _cacheManager: CacheManager | undefined + private _baselineStore: IVectorStore | undefined + private _baselineSignature: string | undefined + private _baselineStamp: string | undefined + private _baselineChecked = 0 + private _baselineSigned = 0 + private _baselineRefresh: Promise | undefined + private _overlay: WorktreeOverlay | undefined private _isRecoveringFromError = false private _retryTimer: ReturnType | undefined private _retryResolve: (() => void) | undefined @@ -41,6 +59,7 @@ export class CodeIndexManager { constructor( public readonly workspacePath: string, private readonly cacheDirectory: string, + public readonly baselinePath?: string, ) { this._stateManager = new CodeIndexStateManager() } @@ -130,7 +149,7 @@ export class CodeIndexManager { } if (event.type !== "error") return - if (event.location !== "orchestrator:startIndexing") return + if (event.location !== "orchestrator:startIndexing" && event.location !== "orchestrator:watcher") return if (!this.isFeatureEnabled || !this.isFeatureConfigured) return if (this._retryTask || this._isRecoveringFromError) return @@ -405,15 +424,13 @@ export class CodeIndexManager { await task } - public dispose(): void { + public async dispose(): Promise { if (this._disposed) return this._disposed = true this.clearRetryTimer() this._retryTask = undefined - // RATIONALE: cancelIndexing() sets _cancelRequested and calls stopWatcher() + - // scanner.cancel(), which cooperatively aborts any in-flight scan. Using only - // stopWatcher() left the orchestrator's _runScan() unaware it should exit. - this._orchestrator?.cancelIndexing() + await this._orchestrator?.shutdown?.() + await this._baselineStore?.close?.() this._stateManager.dispose() this._telemetry.dispose() } @@ -437,30 +454,103 @@ export class CodeIndexManager { public async searchIndex(query: string, directoryPrefix?: string): Promise { if (!this.isFeatureEnabled) return [] this.assertInitialized() + await this.refreshBaseline() return this._searchService!.searchIndex(query, directoryPrefix) } - private async _recreateServices(): Promise { - log.info("starting indexing service recreation", { workspacePath: this.workspacePath }) - this._orchestrator?.stopWatcher() - this._orchestrator = undefined - this._searchService = undefined + private async refreshBaseline(): Promise { + if (!this.baselinePath || this._disposed) return + if (this._baselineRefresh) return this._baselineRefresh + const now = Date.now() + if (now - this._baselineChecked < BASELINE_CHECK_INTERVAL) return + this._baselineChecked = now + const baselinePath = this.baselinePath - this._serviceFactory = new CodeIndexServiceFactory( + const task = (async () => { + const cache = new CacheManager(this.cacheDirectory, baselinePath) + const stamp = await cache.stamp() + const force = now - this._baselineSigned >= BASELINE_SIGNATURE_INTERVAL + if (!force && stamp === this._baselineStamp) return + + await cache.initialize() + const signature = cache.signature() + this._baselineStamp = stamp + this._baselineSigned = now + if (signature === this._baselineSignature && this._baselineStore) return + + const baseline = await this.createBaseline(this._serviceFactory!) + this._baselineStamp = baseline?.stamp ?? stamp + this._baselineSigned = now + if (!baseline?.store) { + if (!this._baselineStore) this._baselineSignature = signature + return + } + + log.info("shared indexing baseline changed; rebuilding worktree delta", { + workspacePath: this.workspacePath, + baselinePath, + }) + await this._recreateServices(baseline) + if (this._disposed) return + await this._orchestrator?.startIndexing("background") + })().finally(() => { + this._baselineRefresh = undefined + }) + this._baselineRefresh = task + return task + } + + private async createBaseline(factory: CodeIndexServiceFactory): Promise { + if (!this.baselinePath) return + + const cache = new CacheManager(this.cacheDirectory, this.baselinePath) + await cache.initialize() + const signature = cache.signature() + const stamp = await cache.stamp() + const hashes = new Map() + for (const [filePath, hash] of Object.entries(cache.getAllHashes())) { + const rel = path.relative(this.baselinePath, filePath) + if (!rel || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) continue + hashes.set(rel.replaceAll("\\", "/"), hash) + } + + const store = factory.createVectorStore(this.baselinePath) + try { + if (!store.openExisting) throw new Error("The configured vector store cannot open a shared baseline") + // Validate compatibility without keeping every worktree baseline connection open. + await store.openExisting() + await store.close?.() + return { + store, + signature, + stamp, + overlay: new WorktreeOverlay(this.workspacePath, this.baselinePath, hashes), + } + } catch (err) { + await store.close?.() + log.warn("shared indexing baseline is unavailable; using an independent worktree index", { + workspacePath: this.workspacePath, + baselinePath: this.baselinePath, + err, + }) + return { signature, stamp } + } + } + + private async _recreateServices(prepared?: Baseline): Promise { + log.info("starting indexing service recreation", { workspacePath: this.workspacePath }) + const factory = new CodeIndexServiceFactory( this._configManager!, this.workspacePath, this._cacheManager!, this.cacheDirectory, (event) => this.handleTelemetry(event), ) - const ignoreInstance = await loadIgnore(this.workspacePath) - const config = this._configManager!.getConfig() - const { embedder, vectorStore, scanner, fileWatcher } = this._serviceFactory.createServices( - this._cacheManager!, - ignoreInstance, - ) + const baseline = prepared ?? (await this.createBaseline(factory)) + const { embedder, vectorStore, scanner, fileWatcher } = factory.createServices(this._cacheManager!, ignoreInstance) + fileWatcher.setOverlay?.(baseline?.overlay) log.info("created indexing services", { workspacePath: this.workspacePath, provider: embedder.embedderInfo.name, @@ -469,13 +559,12 @@ export class CodeIndexManager { }) const shouldValidate = embedder && embedder.embedderInfo.name === config.embedderProvider - if (shouldValidate) { log.info("validating embedder configuration", { workspacePath: this.workspacePath, provider: embedder.embedderInfo.name, }) - const validationResult = await this._serviceFactory.validateEmbedder(embedder) + const validationResult = await factory.validateEmbedder(embedder) if (!validationResult.valid) { const errorMessage = validationResult.error || "Embedder configuration validation failed" this._stateManager.setSystemState("Error", errorMessage) @@ -487,7 +576,7 @@ export class CodeIndexManager { }) } - this._orchestrator = new CodeIndexOrchestrator( + const orchestrator = new CodeIndexOrchestrator( this._configManager!, this._stateManager, this.workspacePath, @@ -496,10 +585,33 @@ export class CodeIndexManager { scanner, fileWatcher, (event) => this.handleTelemetry(event), + baseline?.overlay, + Boolean(this.baselinePath && !baseline?.store), + ) + const search = new CodeIndexSearchService( + this._configManager!, + this._stateManager, + embedder, + vectorStore, + baseline?.store && baseline.overlay ? { store: baseline.store, overlay: baseline.overlay } : undefined, ) - this._searchService = new CodeIndexSearchService(this._configManager!, this._stateManager, embedder, vectorStore) + await this._orchestrator?.shutdown?.() + await this._baselineStore?.close?.() + if (this._disposed) { + await orchestrator.shutdown() + await baseline?.store?.close?.() + return + } + this._serviceFactory = factory + this._orchestrator = orchestrator + this._searchService = search + this._baselineStore = baseline?.store + this._baselineSignature = baseline?.signature + this._baselineStamp = baseline?.stamp + this._baselineSigned = Date.now() + this._overlay = baseline?.overlay this._stateManager.setSystemState("Standby", "") log.info("indexing services are ready", { workspacePath: this.workspacePath }) } diff --git a/packages/kilo-indexing/src/indexing/orchestrator.ts b/packages/kilo-indexing/src/indexing/orchestrator.ts index 06c1f97d41b..e842dee17f7 100644 --- a/packages/kilo-indexing/src/indexing/orchestrator.ts +++ b/packages/kilo-indexing/src/indexing/orchestrator.ts @@ -17,6 +17,7 @@ import type { Disposable } from "./runtime" import { Log } from "../util/log" import { sanitizeErrorMessage } from "./shared/validation-helpers" import { DEFAULT_VECTOR_STORE } from "./constants" +import type { WorktreeOverlay } from "./worktree-overlay" const log = Log.create({ service: "indexing-orchestrator" }) @@ -24,6 +25,7 @@ export class CodeIndexOrchestrator { private _fileWatcherSubscriptions: Disposable[] = [] private _isProcessing = false private _cancelRequested = false + private _active?: Promise constructor( private readonly configManager: CodeIndexConfigManager, @@ -34,6 +36,8 @@ export class CodeIndexOrchestrator { private readonly scanner: DirectoryScanner, private readonly fileWatcher: IFileWatcher, private readonly onTelemetry?: IndexingTelemetryReporter, + private readonly overlay?: WorktreeOverlay, + private readonly independent = false, ) {} private getTelemetryMeta(): IndexingTelemetryMeta { @@ -82,6 +86,9 @@ export class CodeIndexOrchestrator { this.stateManager.setSystemState("Indexing", "Initializing file watcher...") try { + this.fileWatcher.setCollecting(false) + for (const sub of this._fileWatcherSubscriptions) sub.dispose() + this._fileWatcherSubscriptions = [] await this.fileWatcher.initialize() log.info("file watcher initialized", { workspacePath: this.workspacePath }) @@ -106,6 +113,7 @@ export class CodeIndexOrchestrator { workspacePath: this.workspacePath, totalInBatch, }) + if (this.stateManager.state === "Error") return if (totalInBatch > 0) { this.stateManager.setSystemState("Indexed", "File changes processed. Index up-to-date.") } else if (this.stateManager.state === "Indexing") { @@ -114,9 +122,11 @@ export class CodeIndexOrchestrator { } }), this.fileWatcher.onDidFinishBatchProcessing.on((summary: BatchProcessingSummary) => { - if (summary.batchError) { - log.error("batch processing failed", { err: summary.batchError }) - } + if (!summary.batchError) return + log.error("batch processing failed", { err: summary.batchError }) + this.overlay?.prepare() + this.stateManager.setSystemState("Error", `Failed to process file changes: ${summary.batchError.message}`) + this.emitError("orchestrator:watcher", summary.batchError, "watcher") }), ] this.fileWatcher.setCollecting(false) @@ -127,7 +137,16 @@ export class CodeIndexOrchestrator { } } - public async startIndexing(trigger: IndexingTelemetryTrigger = "background"): Promise { + public startIndexing(trigger: IndexingTelemetryTrigger = "background"): Promise { + if (this._active) return this._active + const task = this.runIndexing(trigger).finally(() => { + if (this._active === task) this._active = undefined + }) + this._active = task + return task + } + + private async runIndexing(trigger: IndexingTelemetryTrigger): Promise { log.info("indexing start requested", { workspacePath: this.workspacePath, state: this.stateManager.state, @@ -166,6 +185,7 @@ export class CodeIndexOrchestrator { let mode: IndexingTelemetryMode | undefined try { + this.overlay?.prepare() await this._startWatcher() if (this._cancelRequested) { @@ -183,12 +203,27 @@ export class CodeIndexOrchestrator { return } - if (collectionCreated) { + if (this.overlay) { + if (!collectionCreated) await this.vectorStore.clearCollection() await this.cacheManager.clearCacheFile() - log.info("cleared indexing cache after new collection creation", { workspacePath: this.workspacePath }) + this.cacheManager.seedHashes(this.overlay.seed()) + await this.cacheManager.flush?.() + log.info("seeded worktree index from shared baseline", { + workspacePath: this.workspacePath, + baselinePath: this.overlay.baselinePath, + files: this.overlay.baseline.size, + }) } - const hasExistingData = await this.vectorStore.hasIndexedData() + const hasExistingData = this.overlay || this.independent ? false : await this.vectorStore.hasIndexedData() + if (!this.overlay && !hasExistingData) { + if (!collectionCreated) await this.vectorStore.clearCollection() + await this.cacheManager.clearCacheFile() + log.info("cleared indexing cache before full scan", { + workspacePath: this.workspacePath, + collectionCreated, + }) + } log.info("checked vector store indexed data", { workspacePath: this.workspacePath, hasExistingData, @@ -292,6 +327,10 @@ export class CodeIndexOrchestrator { return } + if (this.overlay && batchErrors.length > 0) { + throw batchErrors[0] + } + if (mode === "full") { // Validate full scan results if (cumulativeFilesIndexed === 0 && cumulativeFilesFound > 0) { @@ -312,8 +351,11 @@ export class CodeIndexOrchestrator { } } - this.fileWatcher.setCollecting(true) + this.overlay?.reconcile(this.cacheManager.getAllHashes()) + await this.cacheManager.flush?.() await this.vectorStore.markIndexingComplete() + await this.vectorStore.close?.() + this.fileWatcher.setCollecting(true) this.stateManager.setSystemState("Indexed", "File watcher started. Index up-to-date.") log.info("workspace scan finalized", { workspacePath: this.workspacePath, @@ -335,6 +377,19 @@ export class CodeIndexOrchestrator { }) } + public async shutdown(): Promise { + this._cancelRequested = true + this.scanner.cancel() + this.fileWatcher.setCollecting(false) + await this._active + for (const sub of this._fileWatcherSubscriptions) sub.dispose() + this._fileWatcherSubscriptions = [] + if (this.fileWatcher.shutdown) await this.fileWatcher.shutdown() + else this.fileWatcher.dispose() + await this.vectorStore.close?.() + this._isProcessing = false + } + public stopWatcher(): void { log.info("stopping file watcher", { workspacePath: this.workspacePath }) this.fileWatcher.dispose() diff --git a/packages/kilo-indexing/src/indexing/processors/file-watcher.ts b/packages/kilo-indexing/src/indexing/processors/file-watcher.ts index 22ece9fd5d3..eda57909ab6 100644 --- a/packages/kilo-indexing/src/indexing/processors/file-watcher.ts +++ b/packages/kilo-indexing/src/indexing/processors/file-watcher.ts @@ -31,6 +31,7 @@ import { } from "../shared/get-relative-path" import { FileIgnore } from "../../file/ignore" import { Log } from "../../util/log" +import type { WorktreeOverlay } from "../worktree-overlay" import { sanitizeErrorMessage } from "../shared/validation-helpers" const log = Log.create({ service: "file-watcher" }) @@ -50,9 +51,11 @@ export class FileWatcher implements IFileWatcher { private readonly FILE_PROCESSING_CONCURRENCY_LIMIT = 10 private batchSegmentThreshold: number private maxBatchRetries: number - private collecting = true + private collecting = false private draining = false + private drainTask?: Promise private ready?: Promise + private overlay?: WorktreeOverlay public readonly onDidStartBatchProcessing = new Emitter() public readonly onBatchProgressUpdate = new Emitter<{ @@ -150,8 +153,16 @@ export class FileWatcher implements IFileWatcher { log.info("file watcher ready", { workspacePath: this.workspacePath }) } + setOverlay(overlay?: WorktreeOverlay): void { + this.overlay = overlay + } + setCollecting(collecting: boolean): void { this.collecting = collecting + if (!collecting && this.batchProcessDebounceTimer) { + clearTimeout(this.batchProcessDebounceTimer) + this.batchProcessDebounceTimer = undefined + } log.info("updated watcher collection mode", { workspacePath: this.workspacePath, collecting, @@ -170,11 +181,20 @@ export class FileWatcher implements IFileWatcher { /** * Disposes the file watcher and cleans up resources. */ + async shutdown(): Promise { + this.collecting = false + if (this.batchProcessDebounceTimer) clearTimeout(this.batchProcessDebounceTimer) + this.batchProcessDebounceTimer = undefined + await this.watcher?.close() + await this.drainTask + this.dispose() + } + dispose(): void { - this.watcher?.close() - if (this.batchProcessDebounceTimer) { - clearTimeout(this.batchProcessDebounceTimer) - } + this.collecting = false + void this.watcher?.close() + if (this.batchProcessDebounceTimer) clearTimeout(this.batchProcessDebounceTimer) + this.batchProcessDebounceTimer = undefined this.onDidStartBatchProcessing.dispose() this.onBatchProgressUpdate.dispose() this.onDidFinishBatchProcessing.dispose() @@ -187,6 +207,7 @@ export class FileWatcher implements IFileWatcher { */ private handleFileEvent(filePath: string, type: "create" | "change" | "delete"): void { if (!this.shouldIndex(filePath)) return + this.overlay?.block(filePath) this.accumulatedEvents.set(filePath, { path: filePath, type }) if (!this.collecting) return this.scheduleBatchProcessing() @@ -196,11 +217,22 @@ export class FileWatcher implements IFileWatcher { * Schedules batch processing with debounce. */ private scheduleBatchProcessing(): void { - if (!this.collecting) return + if (!this.collecting || this.drainTask) return if (this.batchProcessDebounceTimer) { clearTimeout(this.batchProcessDebounceTimer) } - this.batchProcessDebounceTimer = setTimeout(() => this.triggerBatchProcessing(), this.BATCH_DEBOUNCE_DELAY_MS) + this.batchProcessDebounceTimer = setTimeout(() => { + this.batchProcessDebounceTimer = undefined + const task = this.triggerBatchProcessing().catch((err) => { + const error = err instanceof Error ? err : new Error(String(err)) + this.collecting = false + this.onDidFinishBatchProcessing.fire({ processedFiles: [], batchError: error }) + }) + this.drainTask = task.finally(() => { + this.drainTask = undefined + if (this.collecting && this.accumulatedEvents.size > 0) this.scheduleBatchProcessing() + }) + }, this.BATCH_DEBOUNCE_DELAY_MS) } /** @@ -217,17 +249,20 @@ export class FileWatcher implements IFileWatcher { pendingEvents: this.accumulatedEvents.size, }) - while (this.collecting && this.accumulatedEvents.size > 0) { - const eventsToProcess = new Map(this.accumulatedEvents) - this.accumulatedEvents.clear() + try { + while (this.collecting && this.accumulatedEvents.size > 0) { + const eventsToProcess = new Map(this.accumulatedEvents) + this.accumulatedEvents.clear() - const filePathsInBatch = Array.from(eventsToProcess.keys()) - this.onDidStartBatchProcessing.fire(filePathsInBatch) - await this.processBatch(eventsToProcess) + const filePathsInBatch = Array.from(eventsToProcess.keys()) + this.onDidStartBatchProcessing.fire(filePathsInBatch) + await this.processBatch(eventsToProcess) + } + } finally { + this.draining = false + if (this.collecting && this.accumulatedEvents.size > 0) this.scheduleBatchProcessing() + log.info("completed watcher event drain", { workspacePath: this.workspacePath }) } - - this.draining = false - log.info("completed watcher event drain", { workspacePath: this.workspacePath }) } private shouldIndex(filePath: string) { @@ -414,45 +449,49 @@ export class FileWatcher implements IFileWatcher { batchResults: FileProcessingResult[], overallBatchError?: Error, ): Promise { - if (pointsForBatchUpsert.length > 0 && this.vectorStore && !overallBatchError) { + if (!overallBatchError) { try { - for (let i = 0; i < pointsForBatchUpsert.length; i += this.batchSegmentThreshold) { - const batch = pointsForBatchUpsert.slice(i, i + this.batchSegmentThreshold) - let retryCount = 0 - let upsertError: Error | undefined + if (pointsForBatchUpsert.length > 0 && this.vectorStore) { + for (let i = 0; i < pointsForBatchUpsert.length; i += this.batchSegmentThreshold) { + const batch = pointsForBatchUpsert.slice(i, i + this.batchSegmentThreshold) + let retryCount = 0 + let upsertError: Error | undefined - while (retryCount < this.maxBatchRetries) { - try { - await this.vectorStore.upsertPoints(batch) - break - } catch (error) { - upsertError = error as Error - retryCount++ - if (retryCount === this.maxBatchRetries) { - log.error("upsert retry exhausted", { - error: sanitizeErrorMessage(upsertError.message), - location: "upsertPoints", - errorType: "upsert_retry_exhausted", - retryCount: this.maxBatchRetries, - }) - this.emitError("file-watcher:upsert_retry_exhausted", upsertError, this.maxBatchRetries) - throw new Error(`Failed to upsert batch after ${this.maxBatchRetries} retries: ${upsertError.message}`) + while (retryCount < this.maxBatchRetries) { + try { + await this.vectorStore.upsertPoints(batch) + break + } catch (error) { + upsertError = error as Error + retryCount++ + if (retryCount === this.maxBatchRetries) { + log.error("upsert retry exhausted", { + error: sanitizeErrorMessage(upsertError.message), + location: "upsertPoints", + errorType: "upsert_retry_exhausted", + retryCount: this.maxBatchRetries, + }) + this.emitError("file-watcher:upsert_retry_exhausted", upsertError, this.maxBatchRetries) + throw new Error( + `Failed to upsert batch after ${this.maxBatchRetries} retries: ${upsertError.message}`, + ) + } + this.emitRetry(retryCount, batch.length, upsertError) + await new Promise((resolve) => + setTimeout(resolve, INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount - 1)), + ) } - this.emitRetry(retryCount, batch.length, upsertError) - await new Promise((resolve) => setTimeout(resolve, INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount - 1))) } } } - for (const { path, newHash } of successfullyProcessedForUpsert) { - if (newHash) { - this.cacheManager.updateHash(path, newHash) - } - batchResults.push({ path, status: "success" }) + for (const item of successfullyProcessedForUpsert) { + if (item.newHash) this.cacheManager.updateHash(item.path, item.newHash) + batchResults.push({ path: item.path, status: "success", newHash: item.newHash }) } } catch (error) { const err = error as Error - overallBatchError = overallBatchError || err + overallBatchError = err this.emitError("file-watcher:batch_upsert_error", err) log.error("batch upsert error", { error: sanitizeErrorMessage(err.message), @@ -460,13 +499,13 @@ export class FileWatcher implements IFileWatcher { errorType: "batch_upsert_error", affectedFiles: successfullyProcessedForUpsert.length, }) - for (const { path } of successfullyProcessedForUpsert) { - batchResults.push({ path, status: "error", error: err }) + for (const item of successfullyProcessedForUpsert) { + batchResults.push({ path: item.path, status: "error", error: err }) } } - } else if (overallBatchError && pointsForBatchUpsert.length > 0) { - for (const { path } of successfullyProcessedForUpsert) { - batchResults.push({ path, status: "error", error: overallBatchError }) + } else { + for (const item of successfullyProcessedForUpsert) { + batchResults.push({ path: item.path, status: "error", error: overallBatchError }) } } @@ -497,16 +536,33 @@ export class FileWatcher implements IFileWatcher { // Categorize events const pathsToExplicitlyDelete: string[] = [] const filesToUpsertDetails: Array<{ path: string; originalType: "create" | "change" }> = [] + const reverts = new Map() for (const event of eventsToProcess.values()) { if (event.type === "delete") { pathsToExplicitlyDelete.push(event.path) - } else { - filesToUpsertDetails.push({ - path: event.path, - originalType: event.type, - }) + continue } + + const cached = this.cacheManager.getHash(event.path) + const hash = await readFile(event.path, "utf-8") + .then((content) => createHash("sha256").update(content).digest("hex")) + .catch(() => undefined) + if (cached && hash === cached) { + batchResults.push({ path: event.path, status: "success", newHash: cached }) + processedCountInBatch++ + continue + } + if (hash && hash === this.overlay?.baselineHash(event.path)) { + pathsToExplicitlyDelete.push(event.path) + reverts.set(event.path, hash) + continue + } + + filesToUpsertDetails.push({ + path: event.path, + originalType: event.type, + }) } log.info("processing file watcher batch", { @@ -526,6 +582,9 @@ export class FileWatcher implements IFileWatcher { ) overallBatchError = deletionError processedCountInBatch = deletionCount + if (!deletionError) { + for (const [filePath, hash] of reverts) this.cacheManager.updateHash(filePath, hash) + } // Phase 2: Process files and prepare upserts const { @@ -549,6 +608,16 @@ export class FileWatcher implements IFileWatcher { overallBatchError, ) + const resultError = batchResults.find((item) => item.status === "error" || item.status === "local_error")?.error + overallBatchError ??= resultError + await this.cacheManager.flush() + + for (const event of eventsToProcess.values()) { + const result = batchResults.findLast((item) => item.path === event.path) + if (result?.status !== "success") continue + this.overlay?.settle(event.path, this.cacheManager.getHash(event.path), this.accumulatedEvents.has(event.path)) + } + // Finalize this.onDidFinishBatchProcessing.fire({ processedFiles: batchResults, @@ -668,6 +737,7 @@ export class FileWatcher implements IFileWatcher { vector, payload: { filePath: generateRelativeFilePath(normalizedAbsolutePath, this.workspacePath), + fileHash: block.fileHash, codeChunk: block.content, startLine: block.start_line, endLine: block.end_line, diff --git a/packages/kilo-indexing/src/indexing/processors/scanner.ts b/packages/kilo-indexing/src/indexing/processors/scanner.ts index 0c137589689..564c0d47c60 100644 --- a/packages/kilo-indexing/src/indexing/processors/scanner.ts +++ b/packages/kilo-indexing/src/indexing/processors/scanner.ts @@ -295,6 +295,10 @@ export class DirectoryScanner implements IDirectoryScanner { onFileParsed?.() processedCount++ + if (!isNewFile && this.vectorStore) { + await this.vectorStore.deletePointsByMultipleFilePaths([filePath]) + } + // Process embeddings if configured if (this.embedder && this.vectorStore && blocks.length > 0) { // Add to batch accumulators @@ -303,7 +307,7 @@ export class DirectoryScanner implements IDirectoryScanner { const info = { filePath, fileHash: currentFileHash, - isNew: isNewFile, + isNew: true, } for (const block of blocks) { if (this._cancelled) break @@ -608,6 +612,7 @@ export class DirectoryScanner implements IDirectoryScanner { vector, payload: { filePath: generateRelativeFilePath(normalizedAbsolutePath, scanWorkspace), + fileHash: block.fileHash, codeChunk: block.content, startLine: block.start_line, endLine: block.end_line, diff --git a/packages/kilo-indexing/src/indexing/search-service.ts b/packages/kilo-indexing/src/indexing/search-service.ts index 954ba7e0ea7..139c1a41a85 100644 --- a/packages/kilo-indexing/src/indexing/search-service.ts +++ b/packages/kilo-indexing/src/indexing/search-service.ts @@ -5,6 +5,12 @@ import type { IVectorStore } from "./interfaces/vector-store" import type { CodeIndexConfigManager } from "./config-manager" import type { CodeIndexStateManager } from "./state-manager" import { Log } from "../util/log" +import type { WorktreeOverlay } from "./worktree-overlay" + +interface BaselineSearch { + store: IVectorStore + overlay: WorktreeOverlay +} const log = Log.create({ service: "indexing-search" }) @@ -14,6 +20,7 @@ export class CodeIndexSearchService { private readonly stateManager: CodeIndexStateManager, private readonly embedder: IEmbedder, private readonly vectorStore: IVectorStore, + private readonly baseline?: BaselineSearch, ) {} public async searchIndex(query: string, directoryPrefix?: string): Promise { @@ -37,7 +44,44 @@ export class CodeIndexSearchService { } const normalizedPrefix = directoryPrefix ? path.normalize(directoryPrefix) : undefined - return await this.vectorStore.search(vector, normalizedPrefix, minScore, maxResults) + if (!this.baseline) return await this.vectorStore.search(vector, normalizedPrefix, minScore, maxResults) + if (!this.baseline.overlay.ready) throw new Error("Worktree index reconciliation is not complete.") + + const ceiling = Math.max(maxResults, Math.min(maxResults * 16, 1000)) + const delta = (async () => { + const search = async (limit: number): Promise => { + const results = await this.vectorStore.search(vector, normalizedPrefix, minScore, limit) + const filtered = results.filter((result) => this.baseline!.overlay.deltaResult(result)) + if (filtered.length >= maxResults || results.length < limit || limit >= ceiling) return filtered + return search(Math.min(limit * 2, ceiling)) + } + return search(maxResults) + })() + const base = (async () => { + const checks = new Map>() + const search = async (limit: number): Promise => { + const results = await this.baseline!.store.search(vector, normalizedPrefix, minScore, limit) + const accepted = await Promise.all( + results.map((result) => this.baseline!.overlay.baselineResult(result, checks)), + ) + const filtered = results.filter((_, index) => accepted[index]) + if (filtered.length >= maxResults || results.length < limit || limit >= ceiling) return filtered + return search(Math.min(limit * 2, ceiling)) + } + return search(maxResults) + })() + const [baseline, current] = await Promise.all([base, delta]) + const merged = new Map() + const key = (result: VectorStoreSearchResult) => + [result.payload?.filePath, result.payload?.startLine, result.payload?.endLine, result.payload?.codeChunk].join( + "\0", + ) + + for (const result of baseline) merged.set(key(result), result) + for (const result of current) { + if (this.baseline.overlay.deltaResult(result)) merged.set(key(result), result) + } + return [...merged.values()].sort((left, right) => right.score - left.score).slice(0, maxResults) } catch (err) { log.error("search failed", { err }) throw err diff --git a/packages/kilo-indexing/src/indexing/service-factory.ts b/packages/kilo-indexing/src/indexing/service-factory.ts index 826cc37d3c2..af4ac4cbf3b 100644 --- a/packages/kilo-indexing/src/indexing/service-factory.ts +++ b/packages/kilo-indexing/src/indexing/service-factory.ts @@ -169,7 +169,7 @@ export class CodeIndexServiceFactory { } } - public createVectorStore(): IVectorStore { + public createVectorStore(workspacePath = this.workspacePath): IVectorStore { const config = this.configManager.getConfig() const profile = resolveEmbeddingProfile(config.embedderProvider, config.modelId, config.modelDimension) @@ -191,7 +191,7 @@ export class CodeIndexServiceFactory { vectorSize: profile.dimension, dbDir, }) - return new LanceDBVectorStore(this.workspacePath, profile.dimension, dbDir, profile) + return new LanceDBVectorStore(workspacePath, profile.dimension, dbDir, profile) } if (!config.qdrantUrl) throw new Error("Qdrant URL is required.") @@ -201,7 +201,7 @@ export class CodeIndexServiceFactory { model: profile.modelId, vectorSize: profile.dimension, }) - return new QdrantVectorStore(this.workspacePath, config.qdrantUrl, profile.dimension, config.qdrantApiKey, profile) + return new QdrantVectorStore(workspacePath, config.qdrantUrl, profile.dimension, config.qdrantApiKey, profile) } public createDirectoryScanner( diff --git a/packages/kilo-indexing/src/indexing/vector-store/lancedb-vector-store.ts b/packages/kilo-indexing/src/indexing/vector-store/lancedb-vector-store.ts index fc607371f9c..f97363af68c 100644 --- a/packages/kilo-indexing/src/indexing/vector-store/lancedb-vector-store.ts +++ b/packages/kilo-indexing/src/indexing/vector-store/lancedb-vector-store.ts @@ -10,8 +10,20 @@ import type { EmbeddingProfile } from "../embedding-profile" import { loadLanceDB } from "./lancedb-loader" const log = Log.create({ service: "lancedb-store" }) +let nativeQueue = Promise.resolve() +function native(run: () => Promise): Promise { + const task = nativeQueue.then(run) + nativeQueue = task.then( + () => undefined, + () => undefined, + ) + return task +} + +const SCHEMA = "2" const KEY = { + schema: "index_schema", size: "vector_size", complete: "indexing_complete", provider: "embedding_provider", @@ -74,19 +86,16 @@ export class LanceDBVectorStore implements IVectorStore { * @returns The LanceDB connection. */ private async getDb(): Promise { - if (this.db) { - return this.db - } + if (this.db) return this.db - const lancedb = await this.loadLanceDBModule() + return native(async () => { + if (this.db) return this.db + const lancedb = await this.loadLanceDBModule() - // Create parent directory if needed - if (!fs.existsSync(this.dbPath)) { - fs.mkdirSync(this.dbPath, { recursive: true }) - } - - this.db = await lancedb.connect(this.dbPath) - return this.db as Connection + if (!fs.existsSync(this.dbPath)) fs.mkdirSync(this.dbPath, { recursive: true }) + this.db = await lancedb.connect(this.dbPath) + return this.db as Connection + }) } /** @@ -102,7 +111,7 @@ export class LanceDBVectorStore implements IVectorStore { try { // Try to open existing table - const table = await db.openTable(this.vectorTableName) + const table = await native(() => db.openTable(this.vectorTableName)) this.table = table return table } catch (error) { @@ -121,6 +130,7 @@ export class LanceDBVectorStore implements IVectorStore { id: "sample", vector: new Array(this.vectorSize).fill(0), filePath: "sample", + fileHash: "sample", codeChunk: "sample", startLine: 0, endLine: 0, @@ -134,6 +144,10 @@ export class LanceDBVectorStore implements IVectorStore { */ private _createMetadataData() { return [ + { + key: KEY.schema, + value: SCHEMA, + }, { key: KEY.size, value: String(this.vectorSize), @@ -162,7 +176,7 @@ export class LanceDBVectorStore implements IVectorStore { * @param db The LanceDB connection. */ private async _createVectorTable(db: Connection): Promise { - this.table = await db.createTable(this.vectorTableName, this._createSampleData()) + this.table = await native(() => db.createTable(this.vectorTableName, this._createSampleData())) if (this.table) { await this.table.delete("id = 'sample'") } @@ -173,7 +187,7 @@ export class LanceDBVectorStore implements IVectorStore { * @param db The LanceDB connection. */ private async _createMetadataTable(db: Connection): Promise { - await db.createTable(this.metadataTableName, this._createMetadataData()) + await native(() => db.createTable(this.metadataTableName, this._createMetadataData())) } /** @@ -194,15 +208,10 @@ export class LanceDBVectorStore implements IVectorStore { * @returns The stored vector size, or null if not found. */ private async _getStoredVectorSize(db: Connection): Promise { - try { - const value = await this._getMetadataValue(db, KEY.size) - if (value === undefined) return null - const dim = this._parseNumber(value) - return dim ?? null - } catch (error) { - log.warn("Failed to read metadata table", { error }) - return null - } + const value = await this._getMetadataValue(db, KEY.size) + if (value === undefined) return null + const dim = this._parseNumber(value) + return dim ?? null } private isValidMetadataKey(key: string): boolean { @@ -219,27 +228,22 @@ export class LanceDBVectorStore implements IVectorStore { if (!this.isValidMetadataKey(key)) { throw new Error(`Invalid metadata key: ${key}`) } - const metadataTable = await db.openTable(this.metadataTableName) + const metadataTable = await native(() => db.openTable(this.metadataTableName)) const rows = await metadataTable.query().where(`key = '${key}'`).toArray() return rows.length > 0 ? rows[0].value : undefined } private async _getStoredEmbeddingProfile(db: Connection): Promise { - try { - const provider = await this._getMetadataValue(db, KEY.provider) - const modelId = await this._getMetadataValue(db, KEY.model) - const dimension = await this._getMetadataValue(db, KEY.dimension) - if (typeof provider !== "string" || typeof modelId !== "string") return undefined - const dim = this._parseNumber(dimension) - if (!dim) return undefined - return { - provider: provider as EmbeddingProfile["provider"], - modelId, - dimension: dim, - } - } catch (error) { - log.warn("Failed to read embedding profile metadata", { error }) - return undefined + const provider = await this._getMetadataValue(db, KEY.provider) + const modelId = await this._getMetadataValue(db, KEY.model) + const dimension = await this._getMetadataValue(db, KEY.dimension) + if (typeof provider !== "string" || typeof modelId !== "string") return undefined + const dim = this._parseNumber(dimension) + if (!dim) return undefined + return { + provider: provider as EmbeddingProfile["provider"], + modelId, + dimension: dim, } } @@ -251,6 +255,27 @@ export class LanceDBVectorStore implements IVectorStore { ) } + async openExisting(): Promise { + if (!fs.existsSync(this.dbPath)) throw new Error("Baseline LanceDB store does not exist") + + const db = await this.getDb() + const tables = await db.tableNames() + if (!tables.includes(this.vectorTableName) || !tables.includes(this.metadataTableName)) { + throw new Error("Baseline LanceDB store is incomplete") + } + + const profile = await this._getStoredEmbeddingProfile(db) + if (!profile || !this._isEmbeddingProfileMatch(profile)) { + throw new Error("Baseline LanceDB embedding profile does not match the worktree") + } + + const schema = await this._getMetadataValue(db, KEY.schema) + if (String(schema) !== SCHEMA) throw new Error("Baseline LanceDB index schema does not match the worktree") + const complete = await this._getMetadataValue(db, KEY.complete) + if (String(complete) !== "true") throw new Error("Baseline LanceDB index is not complete") + this.table = await native(() => db.openTable(this.vectorTableName)) + } + async initialize(): Promise { try { await this.closeConnect() @@ -274,12 +299,13 @@ export class LanceDBVectorStore implements IVectorStore { return true } - this.table = await db.openTable(this.vectorTableName) + this.table = await native(() => db.openTable(this.vectorTableName)) const storedVectorSize = metadataTableExists ? await this._getStoredVectorSize(db) : null + const storedSchema = metadataTableExists ? await this._getMetadataValue(db, KEY.schema) : undefined const pointCount = await this.table.countRows() - if (storedVectorSize === null || storedVectorSize !== this.vectorSize) { + if (String(storedSchema) !== SCHEMA || storedVectorSize === null || storedVectorSize !== this.vectorSize) { needsRecreation = true } @@ -344,6 +370,7 @@ export class LanceDBVectorStore implements IVectorStore { id: point.id, vector: point.vector, filePath: point.payload.filePath, + fileHash: point.payload.fileHash, codeChunk: point.payload.codeChunk, startLine: point.payload.startLine, endLine: point.payload.endLine, @@ -392,7 +419,7 @@ export class LanceDBVectorStore implements IVectorStore { if (!payload) { return false } - const validKeys = ["filePath", "codeChunk", "startLine", "endLine"] + const validKeys = ["filePath", "fileHash", "codeChunk", "startLine", "endLine"] const hasValidKeys = validKeys.every((key) => key in payload) return hasValidKeys } @@ -431,6 +458,7 @@ export class LanceDBVectorStore implements IVectorStore { score: 1 - result._distance, // Convert distance to similarity score payload: { filePath: result.filePath, + fileHash: result.fileHash, codeChunk: result.codeChunk, startLine: result.startLine, endLine: result.endLine, @@ -501,7 +529,7 @@ export class LanceDBVectorStore implements IVectorStore { const tableNames = await db.tableNames() if (tableNames.includes(this.metadataTableName)) { - const metadataTable = await db.openTable(this.metadataTableName) + const metadataTable = await native(() => db.openTable(this.metadataTableName)) await metadataTable.delete("true") } } catch (metadataError) { @@ -526,6 +554,10 @@ export class LanceDBVectorStore implements IVectorStore { } } + async close(): Promise { + await this.closeConnect() + } + private async closeConnect(): Promise { if (this.table) { this.table = null @@ -570,7 +602,7 @@ export class LanceDBVectorStore implements IVectorStore { }) return false } - const metadataTable = await db.openTable(this.metadataTableName) + const metadataTable = await native(() => db.openTable(this.metadataTableName)) const metadataResults = await metadataTable.query().where(`key = '${KEY.complete}'`).toArray() const indexed = metadataResults.length > 0 ? String(metadataResults[0].value) === "true" : false log.info("LanceDB indexing metadata evaluated", { @@ -580,8 +612,8 @@ export class LanceDBVectorStore implements IVectorStore { }) return indexed } catch (error) { - log.warn("Failed to check if collection has data", { error }) - return false + log.error("Failed to check if collection has data", { error }) + throw error } } @@ -596,6 +628,7 @@ export class LanceDBVectorStore implements IVectorStore { } private async _persistEmbeddingProfile(metadataTable: Table): Promise { + await this._upsertMetadata(metadataTable, KEY.schema, SCHEMA) await this._upsertMetadata(metadataTable, KEY.provider, this.profile.provider) await this._upsertMetadata(metadataTable, KEY.model, this.profile.modelId) await this._upsertMetadata(metadataTable, KEY.dimension, this.profile.dimension) @@ -609,7 +642,7 @@ export class LanceDBVectorStore implements IVectorStore { async markIndexingComplete(): Promise { try { const db = await this.getDb() - const metadataTable = await db.openTable(this.metadataTableName) + const metadataTable = await native(() => db.openTable(this.metadataTableName)) await this._persistEmbeddingProfile(metadataTable) await this._upsertMetadata(metadataTable, KEY.complete, "true") log.info("Marked indexing as complete") @@ -626,7 +659,7 @@ export class LanceDBVectorStore implements IVectorStore { async markIndexingIncomplete(): Promise { try { const db = await this.getDb() - const metadataTable = await db.openTable(this.metadataTableName) + const metadataTable = await native(() => db.openTable(this.metadataTableName)) await this._persistEmbeddingProfile(metadataTable) await this._upsertMetadata(metadataTable, KEY.complete, "false") log.info("Marked indexing as incomplete (in progress)") diff --git a/packages/kilo-indexing/src/indexing/vector-store/qdrant-client.ts b/packages/kilo-indexing/src/indexing/vector-store/qdrant-client.ts index 44b45240966..53a4df91561 100644 --- a/packages/kilo-indexing/src/indexing/vector-store/qdrant-client.ts +++ b/packages/kilo-indexing/src/indexing/vector-store/qdrant-client.ts @@ -9,7 +9,9 @@ import type { EmbeddingProfile } from "../embedding-profile" const log = Log.create({ service: "qdrant-store" }) +const SCHEMA = 2 const KEY = { + schema: "index_schema", complete: "indexing_complete", provider: "embedding_provider", model: "embedding_model_id", @@ -221,12 +223,12 @@ export class QdrantVectorStore implements IVectorStore { }) } - private async recreateCollectionForProfile(stored?: EmbeddingProfile): Promise { + private async recreateCollectionForCompatibility(stored?: EmbeddingProfile): Promise { const from = stored ? `${stored.provider}:${stored.modelId}:${stored.dimension}` : "missing embedding metadata on populated collection" const to = `${this.profile.provider}:${this.profile.modelId}:${this.profile.dimension}` - log.warn(`Collection ${this.collectionName} embedding profile changed (${from} -> ${to}). Recreating collection.`) + log.warn(`Collection ${this.collectionName} is incompatible (${from} -> ${to}). Recreating collection.`) await this.client.deleteCollection(this.collectionName) await new Promise((resolve) => setTimeout(resolve, 100)) @@ -240,6 +242,28 @@ export class QdrantVectorStore implements IVectorStore { return true } + async openExisting(): Promise { + const info = await this.getCollectionInfo() + if (!info) throw new Error("Baseline Qdrant collection does not exist") + + const vectors = info.config?.params?.vectors + const size = + typeof vectors === "number" + ? vectors + : vectors && typeof vectors === "object" && "size" in vectors && typeof vectors.size === "number" + ? vectors.size + : 0 + if (size !== this.vectorSize) throw new Error("Baseline Qdrant vector dimension does not match the worktree") + + const payload = await this.getMetadataPayload() + const profile = this.getStoredProfile(payload) + if (!profile || !this.isProfileMatch(profile)) { + throw new Error("Baseline Qdrant embedding profile does not match the worktree") + } + if (payload?.[KEY.schema] !== SCHEMA) throw new Error("Baseline Qdrant index schema does not match the worktree") + if (payload?.[KEY.complete] !== true) throw new Error("Baseline Qdrant index is not complete") + } + /** * Initializes the vector store * @returns Promise resolving to boolean indicating if a new collection was created @@ -278,8 +302,8 @@ export class QdrantVectorStore implements IVectorStore { } else { const payload = await this.getMetadataPayload() const profile = this.getStoredProfile(payload) - created = - !profile || !this.isProfileMatch(profile) ? await this.recreateCollectionForProfile(profile) : false + const compatible = payload?.[KEY.schema] === SCHEMA && profile && this.isProfileMatch(profile) + created = compatible ? false : await this.recreateCollectionForCompatibility(profile) } } else { // Exists but wrong vector size, recreate with enhanced error handling @@ -453,7 +477,7 @@ export class QdrantVectorStore implements IVectorStore { if (!payload) { return false } - const validKeys = ["filePath", "codeChunk", "startLine", "endLine"] + const validKeys = ["filePath", "fileHash", "codeChunk", "startLine", "endLine"] const hasValidKeys = validKeys.every((key) => key in payload) return hasValidKeys } @@ -523,7 +547,7 @@ export class QdrantVectorStore implements IVectorStore { exact: false, }, with_payload: { - include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + include: ["filePath", "fileHash", "codeChunk", "startLine", "endLine", "pathSegments"], }, } @@ -655,14 +679,7 @@ export class QdrantVectorStore implements IVectorStore { */ async hasIndexedData(): Promise { try { - const collectionInfo = await this.getCollectionInfo() - if (!collectionInfo) { - log.info("Qdrant collection has no indexed data", { - collection: this.collectionName, - reason: "collection_missing", - }) - return false - } + const collectionInfo = await this.client.getCollection(this.collectionName) // Check if the collection has any points indexed const pointsCount = collectionInfo.points_count ?? 0 if (pointsCount === 0) { @@ -692,8 +709,8 @@ export class QdrantVectorStore implements IVectorStore { log.info("No indexing metadata marker found. Using backward compatibility mode (checking points_count > 0).") return pointsCount > 0 } catch (error) { - log.warn("Failed to check if collection has data", { error }) - return false + log.error("Failed to check if collection has data", { error }) + throw error } } @@ -710,6 +727,7 @@ export class QdrantVectorStore implements IVectorStore { vector: new Array(this.vectorSize).fill(0), payload: { type: "metadata", + [KEY.schema]: SCHEMA, [KEY.complete]: true, [KEY.provider]: this.profile.provider, [KEY.model]: this.profile.modelId, @@ -740,6 +758,7 @@ export class QdrantVectorStore implements IVectorStore { vector: new Array(this.vectorSize).fill(0), payload: { type: "metadata", + [KEY.schema]: SCHEMA, [KEY.complete]: false, [KEY.provider]: this.profile.provider, [KEY.model]: this.profile.modelId, diff --git a/packages/kilo-indexing/src/indexing/worktree-overlay.ts b/packages/kilo-indexing/src/indexing/worktree-overlay.ts new file mode 100644 index 00000000000..741bf4ae807 --- /dev/null +++ b/packages/kilo-indexing/src/indexing/worktree-overlay.ts @@ -0,0 +1,92 @@ +import { createHash } from "crypto" +import { readFile } from "fs/promises" +import path from "path" +import type { VectorStoreSearchResult } from "./interfaces/vector-store" + +const normalize = (value: string) => value.replaceAll("\\", "/") + +export class WorktreeOverlay { + readonly shadows = new Set() + readonly blocked = new Set() + ready = false + + constructor( + readonly workspacePath: string, + readonly baselinePath: string, + readonly baseline: ReadonlyMap, + ) {} + + relative(filePath: string): string | undefined { + const rel = path.isAbsolute(filePath) ? path.relative(this.workspacePath, filePath) : filePath + if (!rel || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) return + return normalize(path.normalize(rel)) + } + + baselineHash(filePath: string): string | undefined { + const rel = this.relative(filePath) + if (!rel) return + return this.baseline.get(rel) + } + + seed(): Record { + return Object.fromEntries( + [...this.baseline].map(([filePath, hash]) => [path.join(this.workspacePath, ...filePath.split("/")), hash]), + ) + } + + prepare(): void { + this.ready = false + this.shadows.clear() + this.blocked.clear() + } + + block(filePath: string): void { + const rel = this.relative(filePath) + if (rel) this.blocked.add(rel) + } + + settle(filePath: string, hash: string | undefined, pending = false): void { + const rel = this.relative(filePath) + if (!rel) return + + const baseline = this.baseline.get(rel) + if (baseline !== undefined && baseline !== hash) this.shadows.add(rel) + if (baseline === undefined || baseline === hash) this.shadows.delete(rel) + if (!pending) this.blocked.delete(rel) + } + + reconcile(current: Readonly>): void { + this.shadows.clear() + for (const [filePath, hash] of this.baseline) { + const absolute = path.join(this.workspacePath, ...filePath.split("/")) + if (current[absolute] !== hash) this.shadows.add(filePath) + } + this.ready = true + } + + async baselineResult(result: VectorStoreSearchResult, checks: Map>): Promise { + const filePath = result.payload?.filePath + if (typeof filePath !== "string") return false + const rel = this.relative(filePath) + if (!rel || this.shadows.has(rel) || this.blocked.has(rel)) return false + + const expected = this.baseline.get(rel) + if (!expected || result.payload?.fileHash !== expected) return false + const existing = checks.get(rel) + const valid = + existing ?? + readFile(path.join(this.workspacePath, ...rel.split("/")), "utf-8") + .then((content) => createHash("sha256").update(content).digest("hex") === expected) + .catch(() => false) + checks.set(rel, valid) + return valid + } + + deltaResult(result: VectorStoreSearchResult): boolean { + const filePath = result.payload?.filePath + if (typeof filePath !== "string") return false + const rel = this.relative(filePath) + if (!rel) return false + return !this.blocked.has(rel) + } +} diff --git a/packages/kilo-indexing/test/kilocode/indexing/cache-manager.test.ts b/packages/kilo-indexing/test/kilocode/indexing/cache-manager.test.ts new file mode 100644 index 00000000000..11c6126ac59 --- /dev/null +++ b/packages/kilo-indexing/test/kilocode/indexing/cache-manager.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "bun:test" +import { mkdtemp } from "fs/promises" +import { tmpdir } from "os" +import path from "path" +import { CacheManager } from "../../../src/indexing/cache-manager" + +describe("CacheManager", () => { + test("flushes a stable signature used to detect baseline changes", async () => { + const cacheDir = await mkdtemp(path.join(tmpdir(), "index-cache-")) + const workspace = path.join(cacheDir, "workspace") + const first = new CacheManager(cacheDir, workspace) + await first.initialize() + first.seedHashes({ + [path.join(workspace, "b.ts")]: "b", + [path.join(workspace, "a.ts")]: "a", + }) + await first.flush() + + const second = new CacheManager(cacheDir, workspace) + await second.initialize() + expect(second.signature()).toBe(first.signature()) + + second.updateHash(path.join(workspace, "a.ts"), "changed") + expect(second.signature()).not.toBe(first.signature()) + }) +}) diff --git a/packages/kilo-indexing/test/kilocode/indexing/manager.test.ts b/packages/kilo-indexing/test/kilocode/indexing/manager.test.ts index 96e732e2131..649d4a1444e 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/manager.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/manager.test.ts @@ -1,7 +1,8 @@ -import { describe, expect, test } from "bun:test" +import { describe, expect, spyOn, test } from "bun:test" import { mkdtemp, rm } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" +import { CacheManager } from "../../../src/indexing/cache-manager" import { CodeIndexManager } from "../../../src/indexing/manager" import type { IndexingConfigInput } from "../../../src/indexing/config-manager" import type { IndexingTelemetryEvent, IndexingTelemetryTrigger } from "../../../src/indexing/interfaces/telemetry" @@ -76,6 +77,59 @@ function createStartError(location = "orchestrator:startIndexing"): IndexingTele } describe("CodeIndexManager", () => { + test("falls back when the shared baseline is not ready", async () => { + const mgr = new CodeIndexManager("/tmp/worktree", "/tmp/cache", "/tmp/main") + let closed = 0 + const data = mgr as unknown as { + createBaseline(factory: { createVectorStore(): unknown }): Promise<{ store?: unknown }> + } + const baseline = await data.createBaseline({ + createVectorStore() { + return { + async openExisting() { + throw new Error("baseline rebuilding") + }, + async close() { + closed += 1 + }, + } + }, + }) + + expect(baseline.store).toBeUndefined() + expect(closed).toBe(1) + }) + + test("throttles unchanged baseline cache checks between searches", async () => { + const mgr = new CodeIndexManager("/tmp/worktree", "/tmp/cache", "/tmp/main") + const data = createData(mgr) as Data & { + _baselineStamp: string + _baselineSigned: number + _orchestrator: { state: string } + _searchService: { searchIndex(): Promise<[]> } + } + data._baselineStamp = "same" + data._baselineSigned = Date.now() + data._orchestrator = { state: "Indexed" } + data._searchService = { + async searchIndex() { + return [] + }, + } + const stamp = spyOn(CacheManager.prototype, "stamp").mockResolvedValue("same") + const initialize = spyOn(CacheManager.prototype, "initialize").mockResolvedValue() + + try { + await mgr.searchIndex("first") + await mgr.searchIndex("second") + expect(stamp).toHaveBeenCalledTimes(1) + expect(initialize).not.toHaveBeenCalled() + } finally { + stamp.mockRestore() + initialize.mockRestore() + } + }) + test("returns standby state before services are initialized", () => { const mgr = new CodeIndexManager("/tmp/ws", "/tmp/cache") const data = mgr as unknown as { @@ -315,6 +369,30 @@ describe("CodeIndexManager", () => { expect(calls).toBe(1) }) + test("schedules auto-recovery for watcher failures", async () => { + const mgr = new CodeIndexManager("/tmp/ws", "/tmp/cache") + const data = createData(mgr) + let calls = 0 + + data._recreateServices = async () => { + data._orchestrator = { + state: "Standby", + stopWatcher() {}, + async startIndexing() { + calls += 1 + this.state = "Indexed" + data._stateManager.setSystemState("Indexed", "done") + }, + } + data._searchService = {} + } + + data.handleTelemetry(createStartError("orchestrator:watcher")) + await data._retryTask + + expect(calls).toBe(1) + }) + test("ignores non-orchestrator telemetry errors for auto-recovery", async () => { const mgr = new CodeIndexManager("/tmp/ws", "/tmp/cache") const data = createData(mgr) @@ -393,30 +471,24 @@ describe("CodeIndexManager", () => { expect(mgr.getCurrentStatus().systemStatus).toBe("Indexed") }) - test("dispose calls cancelIndexing on orchestrator", () => { + test("dispose waits for orchestrator shutdown", async () => { const mgr = new CodeIndexManager("/tmp/ws", "/tmp/cache") - let cancel = 0 - let stop = 0 + let shutdown = 0 const data = mgr as unknown as { _orchestrator?: { - stopWatcher(): void - cancelIndexing(): void + shutdown(): Promise } } data._orchestrator = { - stopWatcher() { - stop += 1 - }, - cancelIndexing() { - cancel += 1 + async shutdown() { + shutdown += 1 }, } - mgr.dispose() + await mgr.dispose() - expect(cancel).toBe(1) - expect(stop).toBe(0) + expect(shutdown).toBe(1) }) test("dispose during service recreation cancels the recreated orchestrator", async () => { @@ -456,7 +528,7 @@ describe("CodeIndexManager", () => { const init = mgr.initialize(createInput({ openAiKey: "sk-test" })) await new Promise((resolve) => setTimeout(resolve, 0)) - mgr.dispose() + await mgr.dispose() gate.resolve() await init @@ -484,7 +556,7 @@ describe("CodeIndexManager", () => { const task = data.handleTelemetry(createStartError()) await new Promise((resolve) => setTimeout(resolve, 0)) - mgr.dispose() + await mgr.dispose() gate.resolve() await data._retryTask diff --git a/packages/kilo-indexing/test/kilocode/indexing/orchestrator.test.ts b/packages/kilo-indexing/test/kilocode/indexing/orchestrator.test.ts index 7ce180ff553..4bef1912c7b 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/orchestrator.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/orchestrator.test.ts @@ -17,6 +17,7 @@ import { Emitter } from "../../../src/indexing/runtime" class Store { public clearCount = 0 + public closeCount = 0 public deleteCount = 0 constructor( @@ -47,6 +48,9 @@ class Store { async deleteCollection(): Promise { this.deleteCount += 1 } + async close(): Promise { + this.closeCount += 1 + } async collectionExists(): Promise { return true } @@ -117,6 +121,27 @@ class Watcher { } } +class BlockingScanner { + public isCancelled = false + public finished = false + private readonly gate = Promise.withResolvers() + readonly started = Promise.withResolvers() + + async scanDirectory(): Promise<{ stats: { processed: number; skipped: number }; totalBlockCount: number }> { + this.started.resolve() + await this.gate.promise + this.finished = true + return { stats: { processed: 0, skipped: 0 }, totalBlockCount: 0 } + } + + cancel(): void { + this.isCancelled = true + this.gate.resolve() + } + + updateBatchSegmentThreshold(_newThreshold: number): void {} +} + class FailScanner { public readonly isCancelled = false @@ -225,6 +250,111 @@ describe("CodeIndexOrchestrator telemetry", () => { expect(orchestrator.state).not.toBe("Indexing") }) + test("shutdown waits for an active scan before closing the store", async () => { + const scanner = new BlockingScanner() + const store = new Store(false) + const orchestrator = new CodeIndexOrchestrator( + createConfig(), + new CodeIndexStateManager(), + "/tmp/ws", + { async clearCacheFile() {}, async flush() {} } as unknown as CacheManager, + store as unknown as IVectorStore, + scanner as unknown as DirectoryScanner, + new Watcher() as unknown as IFileWatcher, + ) + + const active = orchestrator.startIndexing("background") + await scanner.started.promise + await orchestrator.shutdown() + await active + + expect(scanner.finished).toBe(true) + expect(store.closeCount).toBe(1) + }) + + test("clears stale vectors and hashes before rebuilding an incomplete store", async () => { + const cache = { + clears: 0, + async clearCacheFile() { + this.clears += 1 + }, + async flush() {}, + } + const store = new Store(false, false) + const orchestrator = new CodeIndexOrchestrator( + createConfig(), + new CodeIndexStateManager(), + "/tmp/ws", + cache as unknown as CacheManager, + store as unknown as IVectorStore, + new Scanner(1, 1, 1) as unknown as DirectoryScanner, + new Watcher() as unknown as IFileWatcher, + ) + + await orchestrator.startIndexing("background") + + expect(store.clearCount).toBe(1) + expect(cache.clears).toBe(1) + expect(orchestrator.state).toBe("Indexed") + }) + + test("does not clear data when index completeness cannot be read", async () => { + const cache = { + clears: 0, + async clearCacheFile() { + this.clears += 1 + }, + } + const store = new Store(true, false) + store.hasIndexedData = async () => { + throw new Error("metadata unavailable") + } + const orchestrator = new CodeIndexOrchestrator( + createConfig(), + new CodeIndexStateManager(), + "/tmp/ws", + cache as unknown as CacheManager, + store as unknown as IVectorStore, + new Scanner(1, 1, 1) as unknown as DirectoryScanner, + new Watcher() as unknown as IFileWatcher, + ) + + await orchestrator.startIndexing("background") + + expect(store.clearCount).toBe(0) + expect(cache.clears).toBe(0) + expect(orchestrator.state).toBe("Error") + }) + + test("rebuilds a complete independent index when the shared baseline is unavailable", async () => { + const cache = { + clears: 0, + async clearCacheFile() { + this.clears += 1 + }, + async flush() {}, + } + const store = new Store(true, false) + const orchestrator = new CodeIndexOrchestrator( + createConfig(), + new CodeIndexStateManager(), + "/tmp/ws", + cache as unknown as CacheManager, + store as unknown as IVectorStore, + new Scanner(1, 1, 1) as unknown as DirectoryScanner, + new Watcher() as unknown as IFileWatcher, + undefined, + undefined, + true, + ) + + await orchestrator.startIndexing("background") + + expect(store.clearCount).toBe(1) + expect(cache.clears).toBe(1) + expect(orchestrator.state).toBe("Indexed") + }) + test("preserves cache and collection data on retryable start failures", async () => { const events: IndexingTelemetryEvent[] = [] const cache = { diff --git a/packages/kilo-indexing/test/kilocode/indexing/processors/file-watcher.test.ts b/packages/kilo-indexing/test/kilocode/indexing/processors/file-watcher.test.ts index 2a66a389abe..688fbb37a2d 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/processors/file-watcher.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/processors/file-watcher.test.ts @@ -2,6 +2,7 @@ import { describe, test, expect } from "bun:test" import { mkdtemp, mkdir, writeFile } from "fs/promises" import { tmpdir } from "os" import path from "path" +import { createHash } from "crypto" import { v5 as uuidv5 } from "uuid" import { CacheManager } from "../../../../src/indexing/cache-manager" import { QDRANT_CODE_BLOCK_NAMESPACE } from "../../../../src/indexing/constants" @@ -14,6 +15,7 @@ import type { } from "../../../../src/indexing/interfaces" import { FileWatcher } from "../../../../src/indexing/processors/file-watcher" import { loadIgnore } from "../../../../src/indexing/shared/load-ignore" +import { WorktreeOverlay } from "../../../../src/indexing/worktree-overlay" function createEmbedder(): IEmbedder { return { @@ -32,6 +34,8 @@ function createEmbedder(): IEmbedder { } class RetryStore implements IVectorStore { + public readonly points: PointStruct[] = [] + constructor(private readonly fail: number) {} private calls = 0 @@ -40,11 +44,12 @@ class RetryStore implements IVectorStore { return false } - async upsertPoints(_points: PointStruct[]): Promise { + async upsertPoints(points: PointStruct[]): Promise { this.calls += 1 if (this.calls <= this.fail) { throw new Error("watcher upsert failure for /tmp/watcher/path.ts") } + this.points.push(...points) } async search( @@ -214,6 +219,86 @@ describe("FileWatcher", () => { expect(error?.error).toContain("[REDACTED_PATH]") }) + test("updates worktree shadows when a baseline file changes and reverts", async () => { + const root = await mkdtemp(path.join(tmpdir(), "file-watcher-test-")) + const cacheDir = path.join(root, ".cache") + const file = path.join(root, "file.ts") + const baseline = "export const baseline = '" + "x".repeat(100) + "'\n" + const changed = "export const changed = '" + "y".repeat(100) + "'\n" + const baselineHash = createHash("sha256").update(baseline).digest("hex") + + await mkdir(cacheDir, { recursive: true }) + await writeFile(file, changed) + + const cache = new CacheManager(cacheDir, root) + await cache.initialize() + cache.seedHashes({ [file]: baselineHash }) + const overlay = new WorktreeOverlay(root, path.join(root, "baseline"), new Map([["file.ts", baselineHash]])) + const store = new RetryStore(0) + const watcher = new FileWatcher(root, cache, createEmbedder(), store) + watcher.setOverlay(overlay) + const data = watcher as unknown as { + processBatch(events: Map): Promise + } + + overlay.block(file) + await data.processBatch(new Map([[file, { path: file, type: "change" }]])) + + expect(overlay.shadows.has("file.ts")).toBe(true) + expect(overlay.blocked.has("file.ts")).toBe(false) + expect(cache.getHash(file)).toBe(createHash("sha256").update(changed).digest("hex")) + + await writeFile(file, baseline) + overlay.block(file) + await data.processBatch(new Map([[file, { path: file, type: "change" }]])) + + expect(overlay.shadows.has("file.ts")).toBe(false) + expect(overlay.blocked.has("file.ts")).toBe(false) + expect(cache.getHash(file)).toBe(baselineHash) + expect(store.points.length).toBeGreaterThan(0) + const count = store.points.length + + await writeFile(file, changed) + overlay.block(file) + await data.processBatch(new Map([[file, { path: file, type: "change" }]])) + await writeFile(file, baseline) + overlay.block(file) + await data.processBatch(new Map([[file, { path: file, type: "change" }]])) + + expect(store.points).toHaveLength(count * 2) + }) + + test("reports unexpected drain failures for recovery", async () => { + const root = await mkdtemp(path.join(tmpdir(), "file-watcher-test-")) + const cacheDir = path.join(root, ".cache") + const file = path.join(root, "file.ts") + await mkdir(cacheDir, { recursive: true }) + await writeFile(file, "export const value = '" + "x".repeat(100) + "'\n") + + const cache = new CacheManager(cacheDir, root) + await cache.initialize() + cache.flush = async () => { + throw new Error("cache flush failed") + } + const watcher = new FileWatcher(root, cache, createEmbedder(), new RetryStore(0)) + const summary = new Promise<{ batchError?: Error }>((resolve) => { + watcher.onDidFinishBatchProcessing.on(resolve) + }) + const data = watcher as unknown as { + handleFileEvent(filePath: string, type: "create" | "change" | "delete"): void + } + + watcher.setCollecting(true) + data.handleFileEvent(file, "create") + const result = await Promise.race([ + summary, + new Promise((_, reject) => setTimeout(() => reject(new Error("watcher did not report failure")), 2000)), + ]) + + expect(result.batchError?.message).toBe("cache flush failed") + await watcher.shutdown() + }) + test("processFile skips files matched by .kilocodeignore during incremental updates", async () => { const root = await mkdtemp(path.join(tmpdir(), "file-watcher-test-")) const cacheDir = path.join(root, ".cache") diff --git a/packages/kilo-indexing/test/kilocode/indexing/processors/scanner.test.ts b/packages/kilo-indexing/test/kilocode/indexing/processors/scanner.test.ts index 2b3a1a0ffe2..65293df016b 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/processors/scanner.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/processors/scanner.test.ts @@ -172,6 +172,40 @@ class RetryStore extends Store { } describe("DirectoryScanner", () => { + test("uses seeded baseline hashes to index only worktree changes", async () => { + const root = await mkdtemp(join(tmpdir(), "scanner-test-")) + const cacheDir = await mkdtemp(join(tmpdir(), "scanner-cache-")) + const same = join(root, "same.ts") + const changed = join(root, "changed.ts") + const added = join(root, "added.ts") + const deleted = join(root, "deleted.ts") + const sameContent = "export const same = 1\n" + const changedContent = "export const changed = 2\n" + + await Bun.write(same, sameContent) + await Bun.write(changed, changedContent) + await Bun.write(added, "export const added = 1\n") + + const cache = new CacheManager(cacheDir, root) + await cache.initialize() + cache.seedHashes({ + [same]: createHash("sha256").update(sameContent).digest("hex"), + [changed]: "baseline-changed", + [deleted]: "baseline-deleted", + }) + + const store = new Store() + const scan = new DirectoryScanner(new Emb(), store, new Parser(), cache, ignore(), 1, 1) + const result = await scan.scanDirectory(root) + + expect(result.stats.processed).toBe(2) + expect(store.points).toBe(2) + expect(cache.getHash(same)).toBe(createHash("sha256").update(sameContent).digest("hex")) + expect(cache.getHash(changed)).toBe(createHash("sha256").update(changedContent).digest("hex")) + expect(cache.getHash(added)).toBeDefined() + expect(cache.getHash(deleted)).toBeUndefined() + }) + test("keeps file metadata when threshold flush is triggered by that file", async () => { const root = await mkdtemp(join(tmpdir(), "scanner-test-")) const cacheDir = await mkdtemp(join(tmpdir(), "scanner-cache-")) diff --git a/packages/kilo-indexing/test/kilocode/indexing/search-service.test.ts b/packages/kilo-indexing/test/kilocode/indexing/search-service.test.ts new file mode 100644 index 00000000000..9447aa688fa --- /dev/null +++ b/packages/kilo-indexing/test/kilocode/indexing/search-service.test.ts @@ -0,0 +1,175 @@ +import { describe, expect, test } from "bun:test" +import { createHash } from "crypto" +import { mkdir, mkdtemp, writeFile } from "fs/promises" +import { tmpdir } from "os" +import path from "path" +import { CodeIndexConfigManager } from "../../../src/indexing/config-manager" +import type { IEmbedder, IVectorStore, VectorStoreSearchResult } from "../../../src/indexing/interfaces" +import { CodeIndexSearchService } from "../../../src/indexing/search-service" +import { CodeIndexStateManager } from "../../../src/indexing/state-manager" +import { WorktreeOverlay } from "../../../src/indexing/worktree-overlay" + +const result = (filePath: string, score: number, codeChunk = filePath, fileHash?: string): VectorStoreSearchResult => ({ + id: `${filePath}:${score}`, + score, + payload: { filePath, fileHash, codeChunk, startLine: 1, endLine: 1 }, +}) + +const store = (results: VectorStoreSearchResult[], limits: number[]): IVectorStore => + ({ + async search(_vector, _prefix, _score, limit) { + limits.push(limit ?? results.length) + return results.slice(0, limit) + }, + }) as unknown as IVectorStore + +const embedder = (calls: string[][]): IEmbedder => ({ + async createEmbeddings(texts) { + calls.push(texts) + return { embeddings: [[1, 2, 3]] } + }, + async validateConfiguration() { + return { valid: true } + }, + get embedderInfo() { + return { name: "openai" } + }, +}) + +const config = () => + new CodeIndexConfigManager({ + enabled: true, + embedderProvider: "openai", + openAiKey: "test", + vectorStoreProvider: "lancedb", + searchMaxResults: 2, + }) + +describe("CodeIndexSearchService worktree search", () => { + test("embeds once, hides baseline paths, and merges the current delta", async () => { + const root = await mkdtemp(path.join(tmpdir(), "search-worktree-")) + const main = await mkdtemp(path.join(tmpdir(), "search-main-")) + const content = "src/base.ts" + const hash = createHash("sha256").update(content).digest("hex") + await mkdir(path.join(root, "src"), { recursive: true }) + await mkdir(path.join(main, "src"), { recursive: true }) + await writeFile(path.join(root, "src/base.ts"), content) + await writeFile(path.join(main, "src/base.ts"), content) + const overlay = new WorktreeOverlay( + root, + main, + new Map([ + ["src/changed.ts", "base"], + ["src/base.ts", hash], + ]), + ) + overlay.reconcile({ + [path.join(root, "src/changed.ts")]: "changed", + [path.join(root, "src/base.ts")]: hash, + }) + + const baseLimits: number[] = [] + const deltaLimits: number[] = [] + const calls: string[][] = [] + const state = new CodeIndexStateManager() + state.setSystemState("Indexed") + const service = new CodeIndexSearchService( + config(), + state, + embedder(calls), + store([result("src/changed.ts", 0.9, "worktree")], deltaLimits), + { + store: store([result("src/changed.ts", 0.99), result("src/base.ts", 0.8, "src/base.ts", hash)], baseLimits), + overlay, + }, + ) + + const results = await service.searchIndex("query") + + expect(calls).toEqual([["query"]]) + expect(results.map((item) => item.payload?.codeChunk)).toEqual(["worktree", "src/base.ts"]) + expect(baseLimits).toEqual([2, 4]) + expect(deltaLimits).toEqual([2]) + }) + + test("rejects live baseline results after the primary checkout changes", async () => { + const root = await mkdtemp(path.join(tmpdir(), "search-worktree-")) + const main = await mkdtemp(path.join(tmpdir(), "search-main-")) + const content = "export const value = 'worktree'" + const hash = createHash("sha256").update(content).digest("hex") + await writeFile(path.join(root, "file.ts"), content) + await writeFile(path.join(main, "file.ts"), "export const value = 'primary-only'") + + const overlay = new WorktreeOverlay(root, main, new Map([["file.ts", hash]])) + overlay.reconcile({ [path.join(root, "file.ts")]: hash }) + const state = new CodeIndexStateManager() + state.setSystemState("Indexed") + const service = new CodeIndexSearchService(config(), state, embedder([]), store([], []), { + store: store( + [ + result( + "file.ts", + 0.99, + "export const value = 'primary-only'", + createHash("sha256").update("export const value = 'primary-only'").digest("hex"), + ), + ], + [], + ), + overlay, + }) + + expect(await service.searchIndex("query")).toEqual([]) + }) + + test("over-fetches when shadowed baseline results consume the first page", async () => { + const root = await mkdtemp(path.join(tmpdir(), "search-worktree-")) + const main = await mkdtemp(path.join(tmpdir(), "search-main-")) + const hash = (content: string) => createHash("sha256").update(content).digest("hex") + await mkdir(path.join(root, "src"), { recursive: true }) + await mkdir(path.join(main, "src"), { recursive: true }) + await Promise.all( + ["c", "d"].flatMap((name) => [ + writeFile(path.join(root, `src/${name}.ts`), `src/${name}.ts`), + writeFile(path.join(main, `src/${name}.ts`), `src/${name}.ts`), + ]), + ) + const overlay = new WorktreeOverlay( + root, + main, + new Map([ + ["src/a.ts", "a"], + ["src/b.ts", "b"], + ["src/c.ts", hash("src/c.ts")], + ["src/d.ts", hash("src/d.ts")], + ]), + ) + overlay.reconcile({ + [path.join(root, "src/a.ts")]: "changed-a", + [path.join(root, "src/b.ts")]: "changed-b", + [path.join(root, "src/c.ts")]: hash("src/c.ts"), + [path.join(root, "src/d.ts")]: hash("src/d.ts"), + }) + + const limits: number[] = [] + const state = new CodeIndexStateManager() + state.setSystemState("Indexed") + const service = new CodeIndexSearchService(config(), state, embedder([]), store([], []), { + store: store( + [ + result("src/a.ts", 0.99), + result("src/b.ts", 0.98), + result("src/c.ts", 0.8, "src/c.ts", hash("src/c.ts")), + result("src/d.ts", 0.7, "src/d.ts", hash("src/d.ts")), + ], + limits, + ), + overlay, + }) + + const results = await service.searchIndex("query") + + expect(limits).toEqual([2, 4]) + expect(results.map((item) => item.payload?.filePath)).toEqual(["src/c.ts", "src/d.ts"]) + }) +}) diff --git a/packages/kilo-indexing/test/kilocode/indexing/vector-store/lancedb-vector-store.test.ts b/packages/kilo-indexing/test/kilocode/indexing/vector-store/lancedb-vector-store.test.ts index 0b47cd6830a..2ac7fdeeab6 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/vector-store/lancedb-vector-store.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/vector-store/lancedb-vector-store.test.ts @@ -173,9 +173,50 @@ describe("LocalVectorStore", () => { expect(mockLoadLanceDB).toHaveBeenCalledTimes(1) expect(mockLanceDBModule.connect).not.toHaveBeenCalled() }) + + test("serializes native connections across stores", async () => { + const other = new LanceDBVectorStore(path.join("mock", "other"), vectorSize, dbDirectory) + store["db"] = null + other["lancedbModule"] = mockLanceDBModule + let active = 0 + let maximum = 0 + mockLanceDBModule.connect.mockImplementation(async () => { + active += 1 + maximum = Math.max(maximum, active) + await Bun.sleep(10) + active -= 1 + return mockDb + }) + + try { + await Promise.all([store.collectionExists(), other.collectionExists()]) + } finally { + await other.close() + } + + expect(maximum).toBe(1) + }) }) describe("initialize", () => { + test("opens a complete compatible baseline without mutating it", async () => { + spyOn(fs, "existsSync").mockReturnValue(true as any) + store["_getStoredEmbeddingProfile"] = mock().mockResolvedValue({ + provider: "openai", + modelId: "", + dimension: vectorSize, + }) + store["_getMetadataValue"] = mock((_: unknown, key: string) => + Promise.resolve(key === "index_schema" ? "2" : "true"), + ) + + await store.openExisting() + + expect(mockDb.createTable).not.toHaveBeenCalled() + expect(mockDb.dropTable).not.toHaveBeenCalled() + expect(mockTable.delete).not.toHaveBeenCalled() + }) + test("should create tables if not exist", async () => { mockDb.tableNames.mockResolvedValue([]) mockDb.createTable.mockResolvedValue(mockTable) @@ -195,19 +236,49 @@ describe("LocalVectorStore", () => { expect(mockDb.dropTable).toHaveBeenCalled() }) - test("should not recreate if vector size matches", async () => { + test("should not recreate if vector size and schema match", async () => { mockDb.tableNames.mockResolvedValue(["vector", "metadata"]) mockDb.openTable.mockResolvedValue(mockTable) store["_getStoredVectorSize"] = mock().mockResolvedValue(vectorSize) + store["_getMetadataValue"] = mock().mockResolvedValue("2") const result = await store.initialize() expect(result).toBe(false) }) + test("recreates an index using the legacy payload schema", async () => { + mockDb.tableNames.mockResolvedValue(["vector", "metadata"]) + mockDb.openTable.mockResolvedValue(mockTable) + store["_getStoredVectorSize"] = mock().mockResolvedValue(vectorSize) + store["_getMetadataValue"] = mock().mockResolvedValue("1") + + expect(await store.initialize()).toBe(true) + expect(mockDb.dropTable).toHaveBeenCalledTimes(2) + }) + test("should throw error on LanceDB failure", async () => { mockDb.tableNames.mockRejectedValue(new Error("fail")) await expect(store.initialize()).rejects.toThrow() }) + test("does not recreate when vector metadata cannot be read", async () => { + store["_getStoredVectorSize"] = mock().mockRejectedValue(new Error("metadata unavailable")) + + await expect(store.initialize()).rejects.toThrow("metadata unavailable") + expect(mockDb.dropTable).not.toHaveBeenCalled() + expect(mockDb.createTable).not.toHaveBeenCalled() + }) + + test("does not recreate when profile metadata cannot be read", async () => { + mockTable.countRows.mockResolvedValue(1) + store["_getStoredVectorSize"] = mock().mockResolvedValue(vectorSize) + store["_getMetadataValue"] = mock().mockResolvedValue("2") + store["_getStoredEmbeddingProfile"] = mock().mockRejectedValue(new Error("profile unavailable")) + + await expect(store.initialize()).rejects.toThrow("profile unavailable") + expect(mockDb.dropTable).not.toHaveBeenCalled() + expect(mockDb.createTable).not.toHaveBeenCalled() + }) + test("should recreate tables when stored embedding identity differs", async () => { const identity = { provider: "openai", @@ -316,7 +387,7 @@ describe("LocalVectorStore", () => { { id: "123e4567-e89b-12d3-a456-426614174000", vector: [1, 2, 3], - payload: { filePath: "a", codeChunk: "b", startLine: 1, endLine: 2 }, + payload: { filePath: "a", fileHash: "hash-a", codeChunk: "b", startLine: 1, endLine: 2 }, }, ] mockTable.delete.mockResolvedValue(undefined) @@ -331,7 +402,7 @@ describe("LocalVectorStore", () => { { id: "123e4567-e89b-12d3-a456-426614174000", vector: [1, 2, 3], - payload: { filePath: "a", codeChunk: "b", startLine: 1, endLine: 2 }, + payload: { filePath: "a", fileHash: "hash-a", codeChunk: "b", startLine: 1, endLine: 2 }, }, ] mockTable.delete.mockResolvedValue(undefined) @@ -349,7 +420,7 @@ describe("LocalVectorStore", () => { distanceRange: distanceRangeSpy, limit: mock().mockReturnThis(), toArray: mock().mockResolvedValue([ - { id: "2", _distance: 0.2, filePath: "a", codeChunk: "c", startLine: 3, endLine: 4 }, + { id: "2", _distance: 0.2, filePath: "a", fileHash: "hash-a", codeChunk: "c", startLine: 3, endLine: 4 }, ]), }) const results = await store.search([1, 2, 3], "a", 0.7, 1) @@ -373,7 +444,7 @@ describe("LocalVectorStore", () => { distanceRange: distanceRangeSpy, limit: mock().mockReturnThis(), toArray: mock().mockResolvedValue([ - { id: "2", _distance: 0.2, filePath: "a", codeChunk: "c", startLine: 3, endLine: 4 }, + { id: "2", _distance: 0.2, filePath: "a", fileHash: "hash-a", codeChunk: "c", startLine: 3, endLine: 4 }, ]), }) const results = await store.search([1, 2, 3], "a", 0.1, 2) @@ -490,7 +561,7 @@ describe("LocalVectorStore", () => { }) test("should return true for valid payload", () => { - const payload: Payload = { filePath: "a", codeChunk: "b", startLine: 1, endLine: 2 } + const payload: Payload = { filePath: "a", fileHash: "hash-a", codeChunk: "b", startLine: 1, endLine: 2 } expect(store["isPayloadValid"](payload)).toBe(true) }) }) @@ -590,6 +661,7 @@ describe("LocalVectorStore", () => { vector: [1, 2, 3], payload: { filePath: "test.ts", + fileHash: "hash-test", codeChunk: "code", startLine: 1, endLine: 2, @@ -609,12 +681,12 @@ describe("LocalVectorStore", () => { { id: "123e4567-e89b-12d3-a456-426614174000", vector: [1, 2, 3], - payload: { filePath: "a", codeChunk: "b", startLine: 1, endLine: 2 }, + payload: { filePath: "a", fileHash: "hash-a", codeChunk: "b", startLine: 1, endLine: 2 }, }, { id: "' OR '1'='1", vector: [4, 5, 6], - payload: { filePath: "c", codeChunk: "d", startLine: 3, endLine: 4 }, + payload: { filePath: "c", fileHash: "hash-c", codeChunk: "d", startLine: 3, endLine: 4 }, }, ] diff --git a/packages/kilo-indexing/test/kilocode/indexing/vector-store/qdrant-client.test.ts b/packages/kilo-indexing/test/kilocode/indexing/vector-store/qdrant-client.test.ts index 8a580bcde83..9ecb36221b2 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/vector-store/qdrant-client.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/vector-store/qdrant-client.test.ts @@ -1,4 +1,5 @@ import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test" +import { createHash } from "crypto" import { DEFAULT_MAX_SEARCH_RESULTS, DEFAULT_SEARCH_MIN_SCORE } from "../../../../src/indexing/constants" @@ -29,14 +30,6 @@ mock.module("@qdrant/js-client-rest", () => ({ QdrantClient: MockQdrantClientConstructor, })) -const mockDigest = mock() -const mockUpdate = mock(() => ({ update: mockUpdate, digest: mockDigest })) -const mockCreateHash = mock(() => ({ update: mockUpdate, digest: mockDigest })) - -mock.module("crypto", () => ({ - createHash: mockCreateHash, -})) - // Now import the module under test import { QdrantVectorStore } from "../../../../src/indexing/vector-store/qdrant-client" @@ -46,8 +39,7 @@ describe("QdrantVectorStore", () => { const mockQdrantUrl = "http://mock-qdrant:6333" const mockApiKey = "test-api-key" const mockVectorSize = 1536 - const mockHashedPath = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" - const expectedCollectionName = `ws-${mockHashedPath.substring(0, 16)}` + const expectedCollectionName = `ws-${createHash("sha256").update(mockWorkspacePath).digest("hex").substring(0, 16)}` beforeEach(() => { // Reset all mocks @@ -61,14 +53,6 @@ describe("QdrantVectorStore", () => { mockQuery.mockReset() mockDelete.mockReset() mockRetrieve.mockReset() - mockCreateHash.mockReset() - mockUpdate.mockReset() - mockDigest.mockReset() - - // Mock crypto.createHash chain - mockCreateHash.mockReturnValue({ update: mockUpdate, digest: mockDigest }) - mockUpdate.mockReturnValue({ update: mockUpdate, digest: mockDigest }) - mockDigest.mockReturnValue(mockHashedPath) vectorStore = new QdrantVectorStore(mockWorkspacePath, mockQdrantUrl, mockVectorSize, mockApiKey) }) @@ -83,9 +67,6 @@ describe("QdrantVectorStore", () => { mockQuery.mockReset() mockDelete.mockReset() mockRetrieve.mockReset() - mockCreateHash.mockReset() - mockUpdate.mockReset() - mockDigest.mockReset() }) test("should correctly initialize QdrantClient and collectionName in constructor", () => { @@ -99,9 +80,6 @@ describe("QdrantVectorStore", () => { "User-Agent": "Kilo-Code", }, }) - expect(mockCreateHash).toHaveBeenCalledWith("sha256") - expect(mockUpdate).toHaveBeenCalledWith(mockWorkspacePath) - expect(mockDigest).toHaveBeenCalledWith("hex") expect((vectorStore as any).collectionName).toBe(expectedCollectionName) expect((vectorStore as any).vectorSize).toBe(mockVectorSize) }) @@ -524,6 +502,31 @@ describe("QdrantVectorStore", () => { }) describe("initialize", () => { + test("opens a complete compatible baseline without mutating it", async () => { + mockGetCollection.mockResolvedValue({ + points_count: 3, + config: { params: { vectors: { size: mockVectorSize } } }, + }) + mockRetrieve.mockResolvedValue([ + { + payload: { + index_schema: 2, + indexing_complete: true, + embedding_provider: "openai", + embedding_model_id: "", + embedding_dimension: mockVectorSize, + }, + }, + ]) + + await vectorStore.openExisting() + + expect(mockCreateCollection).not.toHaveBeenCalled() + expect(mockDeleteCollection).not.toHaveBeenCalled() + expect(mockCreatePayloadIndex).not.toHaveBeenCalled() + expect(mockUpsert).not.toHaveBeenCalled() + }) + test("should create a new collection if none exists and return true", async () => { mockGetCollection.mockRejectedValue({ response: { status: 404 }, @@ -598,6 +601,33 @@ describe("QdrantVectorStore", () => { expect(mockCreatePayloadIndex).toHaveBeenCalledTimes(6) }) + test("recreates a populated collection using the legacy payload schema", async () => { + mockGetCollection + .mockResolvedValueOnce({ + points_count: 7, + config: { params: { vectors: { size: mockVectorSize } } }, + } as any) + .mockRejectedValueOnce({ response: { status: 404 }, message: "Not found" }) + mockRetrieve.mockResolvedValue([ + { + payload: { + index_schema: 1, + indexing_complete: true, + embedding_provider: "openai", + embedding_model_id: "", + embedding_dimension: mockVectorSize, + }, + }, + ] as any) + mockDeleteCollection.mockResolvedValue(true as any) + mockCreateCollection.mockResolvedValue(true as any) + mockCreatePayloadIndex.mockResolvedValue({} as any) + + expect(await vectorStore.initialize()).toBe(true) + expect(mockDeleteCollection).toHaveBeenCalledTimes(1) + expect(mockCreateCollection).toHaveBeenCalledTimes(1) + }) + test("should recreate collection when stored embedding identity mismatches", async () => { const identity = { provider: "openai", @@ -1277,6 +1307,7 @@ describe("QdrantVectorStore", () => { score: 0.85, payload: { filePath: "src/test.ts", + fileHash: "test-hash", codeChunk: "test code", startLine: 1, endLine: 5, @@ -1288,6 +1319,7 @@ describe("QdrantVectorStore", () => { score: 0.75, payload: { filePath: "src/utils.ts", + fileHash: "test-hash", codeChunk: "utility code", startLine: 10, endLine: 15, @@ -1312,7 +1344,7 @@ describe("QdrantVectorStore", () => { exact: false, }, with_payload: { - include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + include: ["filePath", "fileHash", "codeChunk", "startLine", "endLine", "pathSegments"], }, }) expect(callArgs.filter).toEqual({ @@ -1332,6 +1364,7 @@ describe("QdrantVectorStore", () => { score: 0.85, payload: { filePath: "src/components/Button.tsx", + fileHash: "test-hash", codeChunk: "button code", startLine: 1, endLine: 5, @@ -1351,7 +1384,7 @@ describe("QdrantVectorStore", () => { score_threshold: DEFAULT_SEARCH_MIN_SCORE, limit: DEFAULT_MAX_SEARCH_RESULTS, params: { hnsw_ef: 128, exact: false }, - with_payload: { include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"] }, + with_payload: { include: ["filePath", "fileHash", "codeChunk", "startLine", "endLine", "pathSegments"] }, }) expect(callArgs2.filter).toEqual({ must: [ @@ -1383,7 +1416,7 @@ describe("QdrantVectorStore", () => { exact: false, }, with_payload: { - include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + include: ["filePath", "fileHash", "codeChunk", "startLine", "endLine", "pathSegments"], }, }) expect(callArgs3.filter).toEqual({ @@ -1410,7 +1443,7 @@ describe("QdrantVectorStore", () => { exact: false, }, with_payload: { - include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + include: ["filePath", "fileHash", "codeChunk", "startLine", "endLine", "pathSegments"], }, }) expect(callArgs4.filter).toEqual({ @@ -1427,6 +1460,7 @@ describe("QdrantVectorStore", () => { score: 0.85, payload: { filePath: "src/test.ts", + fileHash: "test-hash", codeChunk: "test code", startLine: 1, endLine: 5, @@ -1444,6 +1478,7 @@ describe("QdrantVectorStore", () => { score: 0.55, payload: { filePath: "src/test2.ts", + fileHash: "test-hash", codeChunk: "test code 2", startLine: 10, endLine: 15, @@ -1470,6 +1505,7 @@ describe("QdrantVectorStore", () => { score: 0.85, payload: { filePath: "src/test.ts", + fileHash: "test-hash", codeChunk: "test code", startLine: 1, endLine: 5, @@ -1490,6 +1526,7 @@ describe("QdrantVectorStore", () => { score: 0.55, payload: { filePath: "src/test2.ts", + fileHash: "test-hash", codeChunk: "test code 2", startLine: 10, endLine: 15, @@ -1538,7 +1575,7 @@ describe("QdrantVectorStore", () => { exact: false, }, with_payload: { - include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + include: ["filePath", "fileHash", "codeChunk", "startLine", "endLine", "pathSegments"], }, }) expect(callArgs5.filter).toEqual({ @@ -1586,6 +1623,7 @@ describe("QdrantVectorStore", () => { score: 0.85, payload: { filePath: "src/test.ts", + fileHash: "test-hash", codeChunk: "test code", startLine: 1, endLine: 5, @@ -1609,7 +1647,7 @@ describe("QdrantVectorStore", () => { exact: false, }, with_payload: { - include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + include: ["filePath", "fileHash", "codeChunk", "startLine", "endLine", "pathSegments"], }, }) expect(callArgs7.filter).toEqual({ @@ -1638,7 +1676,7 @@ describe("QdrantVectorStore", () => { exact: false, }, with_payload: { - include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + include: ["filePath", "fileHash", "codeChunk", "startLine", "endLine", "pathSegments"], }, }) expect(callArgs6.filter).toEqual({ @@ -1665,7 +1703,7 @@ describe("QdrantVectorStore", () => { exact: false, }, with_payload: { - include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + include: ["filePath", "fileHash", "codeChunk", "startLine", "endLine", "pathSegments"], }, }) expect(callArgs8.filter).toEqual({ @@ -1692,7 +1730,7 @@ describe("QdrantVectorStore", () => { exact: false, }, with_payload: { - include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + include: ["filePath", "fileHash", "codeChunk", "startLine", "endLine", "pathSegments"], }, }) expect(callArgs9.filter).toEqual({ @@ -1719,7 +1757,7 @@ describe("QdrantVectorStore", () => { exact: false, }, with_payload: { - include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + include: ["filePath", "fileHash", "codeChunk", "startLine", "endLine", "pathSegments"], }, }) expect(callArgs10.filter).toEqual({ @@ -1746,7 +1784,7 @@ describe("QdrantVectorStore", () => { exact: false, }, with_payload: { - include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + include: ["filePath", "fileHash", "codeChunk", "startLine", "endLine", "pathSegments"], }, }) expect(callArgs11.filter).toEqual({ @@ -1779,7 +1817,7 @@ describe("QdrantVectorStore", () => { exact: false, }, with_payload: { - include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"], + include: ["filePath", "fileHash", "codeChunk", "startLine", "endLine", "pathSegments"], }, }) expect(callArgs12.filter).toEqual({ diff --git a/packages/kilo-indexing/test/kilocode/indexing/worktree-overlay.test.ts b/packages/kilo-indexing/test/kilocode/indexing/worktree-overlay.test.ts new file mode 100644 index 00000000000..9cc5920a813 --- /dev/null +++ b/packages/kilo-indexing/test/kilocode/indexing/worktree-overlay.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "bun:test" +import path from "path" +import { WorktreeOverlay } from "../../../src/indexing/worktree-overlay" + +describe("WorktreeOverlay", () => { + test("seeds baseline hashes and shadows changed or deleted files", () => { + const root = path.resolve("/tmp/worktree") + const overlay = new WorktreeOverlay( + root, + path.resolve("/tmp/main"), + new Map([ + ["src/same.ts", "same"], + ["src/changed.ts", "base"], + ["src/deleted.ts", "deleted"], + ]), + ) + + expect(overlay.seed()).toEqual({ + [path.join(root, "src/same.ts")]: "same", + [path.join(root, "src/changed.ts")]: "base", + [path.join(root, "src/deleted.ts")]: "deleted", + }) + + overlay.reconcile({ + [path.join(root, "src/same.ts")]: "same", + [path.join(root, "src/changed.ts")]: "worktree", + }) + + expect(overlay.ready).toBe(true) + expect([...overlay.shadows].sort()).toEqual(["src/changed.ts", "src/deleted.ts"]) + }) + + test("blocks updates and restores baseline visibility after a revert", () => { + const root = path.resolve("/tmp/worktree") + const file = path.join(root, "src/file.ts") + const overlay = new WorktreeOverlay(root, path.resolve("/tmp/main"), new Map([["src/file.ts", "base"]])) + + overlay.block(file) + expect(overlay.blocked.has("src/file.ts")).toBe(true) + + overlay.settle(file, "changed") + expect(overlay.blocked.has("src/file.ts")).toBe(false) + expect(overlay.shadows.has("src/file.ts")).toBe(true) + + overlay.block(file) + overlay.settle(file, "base") + expect(overlay.blocked.has("src/file.ts")).toBe(false) + expect(overlay.shadows.has("src/file.ts")).toBe(false) + }) +}) diff --git a/packages/kilo-jetbrains/AGENTS.md b/packages/kilo-jetbrains/AGENTS.md index 131868988ba..9d026428b88 100644 --- a/packages/kilo-jetbrains/AGENTS.md +++ b/packages/kilo-jetbrains/AGENTS.md @@ -183,6 +183,7 @@ For blocking I/O in coroutines, move the dispatcher switch inside the callee usi - **Typecheck**: `bun run typecheck` or `./gradlew typecheck` from `packages/kilo-jetbrains/` — compiles all Kotlin sources including the generated API client. Does NOT require CLI binaries. - **Full build**: `bun run build` from `packages/kilo-jetbrains/` (prepares CLI binaries + runs Gradle `buildPlugin`). - **Gradle only**: `./gradlew buildPlugin` from `packages/kilo-jetbrains/` (requires CLI binaries already present in `backend/build/generated/cli/`; run `bun run build --prepare-cli` first). +- **Java checks**: Do not run `java -version` as a routine preflight. Gradle commands already fail clearly when Java is missing or incompatible; check Java only when diagnosing that failure mode. - **Via Turbo**: `bun turbo build --filter=@kilocode/kilo-jetbrains` from repo root. - **Run in sandbox**: `./gradlew runIde` — launches sandboxed IntelliJ with the plugin. Does NOT build CLI binaries. - **Run split backend**: `./gradlew runIdeBackend` — if it exits shortly after startup, check for an orphaned Java process from a previous backend run and kill it before restarting. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt index 81958b33b5a..d880bc32225 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt @@ -6,6 +6,9 @@ import ai.kilocode.client.app.Workspace import ai.kilocode.client.session.history.HistoryController import ai.kilocode.client.session.history.HistoryPanel import ai.kilocode.client.telemetry.Telemetry +import ai.kilocode.client.util.UiTimer +import ai.kilocode.client.util.UiTimerSource +import ai.kilocode.client.util.UiTimers import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataProvider import com.intellij.openapi.application.ApplicationManager @@ -20,17 +23,18 @@ import kotlinx.coroutines.cancel import java.awt.BorderLayout import javax.swing.JComponent import javax.swing.JPanel -import javax.swing.Timer class SessionSidePanelManager( private val project: Project, private val root: Workspace, - private val create: (Project, Workspace, SessionManager, SessionRef?) -> SessionUi = { project, workspace, manager, ref -> - service().create(project, workspace, manager, ref) - }, + private val create: (Project, Workspace, SessionManager, SessionRef?, UiTimerSource) -> SessionUi = + { project, workspace, manager, ref, timers -> + service().create(project, workspace, manager, ref, timers) + }, private val resolve: (String) -> Workspace = { dir -> service().workspace(dir) }, private val status: () -> Map = { project.service().activity() }, private val history: ((Disposable, (SessionRef) -> Unit, (String) -> Unit) -> JComponent)? = null, + private val timers: UiTimerSource = UiTimers, ) : SessionManager, Disposable { val component: JPanel = object : JPanel(BorderLayout()), DataProvider { override fun getData(dataId: String): Any? { @@ -42,7 +46,7 @@ class SessionSidePanelManager( private val opened = mutableMapOf() private val all = mutableSetOf() - private val timers = mutableMapOf() + private val activeTimers = mutableMapOf() private var current: SessionUi? = null private var latest: SessionUi? = null private var panel: JComponent? = null @@ -53,7 +57,7 @@ class SessionSidePanelManager( val active = current if (active?.blank == true) return register(active) - show(create(project, root, this, null)) + show(create(project, root, this, null, timers)) } override fun openSession(ref: SessionRef) { @@ -100,7 +104,7 @@ class SessionSidePanelManager( is SessionRef.Local -> ref.session?.directory?.let(resolve) ?: root is SessionRef.Cloud -> root } - return create(project, workspace, this, ref).also { + return create(project, workspace, this, ref, timers).also { all.add(it) opened[ref.key] = it val local = (ref as? SessionRef.Local)?.session?.id @@ -147,7 +151,7 @@ class SessionSidePanelManager( deleted = this::removeSession, ) Disposer.register(this) { cs.cancel() } - return HistoryPanel(this, controller, nav = this::back, manager = this).component + return HistoryPanel(this, controller, nav = this::back, manager = this, timers = timers).component } private fun back() { @@ -206,24 +210,23 @@ class SessionSidePanelManager( private fun schedule(ui: SessionUi) { cancel(ui) val delay = Registry.intValue("kilo.session.inactive.disposeTimeoutMs").coerceAtLeast(0) - val timer = Timer(delay) { - timers.remove(ui) - if (ui === current || ui !in all) return@Timer + val timer = timers.timer(delay, repeats = false) { + activeTimers.remove(ui) + if (ui === current || ui !in all) return@timer disposeUi(ui) } - timer.isRepeats = false - timers[ui] = timer + activeTimers[ui] = timer timer.start() } private fun cancel(ui: SessionUi) { - timers.remove(ui)?.stop() + activeTimers.remove(ui)?.stop() } override fun dispose() { val items = all.toList() - timers.values.forEach { it.stop() } - timers.clear() + activeTimers.values.forEach { it.stop() } + activeTimers.clear() opened.clear() all.clear() current = null diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index 98e63cbca2e..7772ad1dda0 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -41,6 +41,8 @@ import ai.kilocode.client.session.views.question.QuestionView import ai.kilocode.client.settings.profile.UserProfileConfigurable import ai.kilocode.client.telemetry.Telemetry import ai.kilocode.client.ui.layout.Stack +import ai.kilocode.client.util.UiTimerSource +import ai.kilocode.client.util.UiTimers import ai.kilocode.client.vfs.KiloVfsManager import ai.kilocode.log.ChatLogSummary import ai.kilocode.rpc.dto.PromptDto @@ -77,7 +79,6 @@ import java.net.URI import java.nio.file.Path import javax.swing.JComponent import javax.swing.JPanel -import javax.swing.Timer import javax.swing.UIManager /** @@ -97,6 +98,7 @@ class SessionUi( private val manager: SessionManager? = null, private val workspaces: KiloWorkspaceService = service(), private val migration: MigrationUiController = service(), + private val timers: UiTimerSource = UiTimers, ) : JPanel(BorderLayout()), Disposable, SessionEditorStyleTarget, UiDataProvider { companion object { @@ -133,17 +135,16 @@ class SessionUi( afterUpdate = { if (!opening) scroll.followBottom(it) }, loaded = ::onSessionLoaded, openProfileAction = ::openProfileSettings, + timers = timers, ) private lateinit var root: SessionRootPanel private lateinit var account: SessionAccountOverlay private lateinit var drop: SessionDropOverlay - private val hide = Timer(HIDE_MS) { - if (disposed || !this::drop.isInitialized) return@Timer + private val hide = timers.timer(HIDE_MS, repeats = false) { + if (disposed || !this::drop.isInitialized) return@timer drop.setActive(false) - }.apply { - isRepeats = false } private lateinit var sessionContent: JPanel @@ -418,6 +419,7 @@ class SessionUi( history = { manager?.showHistory() }, activity = { manager?.activity() ?: sessions.activity() }, titles = { manager?.titles().orEmpty() }, + timers = timers, ) empty = panel scroll.show(panel.view) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt index 52735be09d5..48ebfa2f5a0 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUiFactory.kt @@ -3,6 +3,8 @@ package ai.kilocode.client.session import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.Workspace +import ai.kilocode.client.util.UiTimerSource +import ai.kilocode.client.util.UiTimers import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.project.Project @@ -19,6 +21,7 @@ class SessionUiFactory( workspace: Workspace, manager: SessionManager, ref: SessionRef? = null, + timers: UiTimerSource = UiTimers, ): SessionUi = SessionUi( project = project, workspace = workspace, @@ -27,6 +30,7 @@ class SessionUiFactory( cs = scope(), ref = ref, manager = manager, + timers = timers, ) fun scope(): CoroutineScope { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/DelayedState.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/DelayedState.kt index 623524e2c0a..960bf3b57a9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/DelayedState.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/DelayedState.kt @@ -1,18 +1,20 @@ package ai.kilocode.client.session.controller +import ai.kilocode.client.util.UiTimerSource +import ai.kilocode.client.util.UiTimers import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager -import javax.swing.Timer internal class DelayedState( private val ms: Long, + private val timers: UiTimerSource = UiTimers, ) : Disposable { private val tick = when { ms <= 0 -> 1 ms > Int.MAX_VALUE -> Int.MAX_VALUE else -> ms.coerceAtMost(TICK_MS).toInt() } - private val timer = Timer(tick) { flush() } + private val timer = timers.timer(tick) { flush() } private val pending = mutableListOf>() @Volatile private var alive = true @@ -36,7 +38,7 @@ internal class DelayedState( } } - internal fun active() = timer.isRunning + internal fun active() = timer.isRunning() private fun apply(item: Pending) { if (!alive) return @@ -47,7 +49,7 @@ internal class DelayedState( private fun flush() { if (!alive) return - val now = System.currentTimeMillis() + val now = timers.now() for (item in pending.toList()) { if (item.due > now) continue apply(item) @@ -56,7 +58,7 @@ internal class DelayedState( } private fun due(): Long { - val now = System.currentTimeMillis() + val now = timers.now() return now + ms.coerceAtMost(Long.MAX_VALUE - now) } @@ -72,9 +74,6 @@ internal class DelayedState( override fun dispose() { alive = false cancel() - edt { - timer.stop() - } } private data class Pending( diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 49d265623b2..7048f8e9aa7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -23,6 +23,8 @@ import ai.kilocode.client.session.model.Text import ai.kilocode.client.plugin.KiloPluginSettings import ai.kilocode.client.session.SessionRef import ai.kilocode.client.telemetry.Telemetry +import ai.kilocode.client.util.UiTimerSource +import ai.kilocode.client.util.UiTimers import ai.kilocode.rpc.dto.ChatEventDto import ai.kilocode.rpc.dto.ConfigWarningDto import ai.kilocode.rpc.dto.ConfigUpdateDto @@ -89,6 +91,7 @@ class SessionController( private val loaded: (Boolean) -> Unit = {}, private val openProfileAction: () -> Unit = {}, private val telemetry: (String, Map) -> Unit = { event, props -> Telemetry.send(event, props) }, + private val timers: UiTimerSource = UiTimers, ) : Disposable { private data class OrganizationTarget(val org: String?) @@ -138,7 +141,7 @@ class SessionController( private var viewState: SessionControllerEvent.ViewChanged? = null private var connectionState: SessionControllerEvent.ConnectionChanged? = null private var connectionTargetState: SessionControllerEvent.ConnectionChanged? = null - private val connectionDelay = DelayedState(displayMs) + private val connectionDelay = DelayedState(displayMs, timers) private var acctState: SessionControllerEvent.AccountOverlayChanged = SessionControllerEvent.AccountOverlayChanged.Hide private var acctAllowed = false diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index 34147ce19e0..3d969a291e7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -7,6 +7,8 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.HoverIcon import ai.kilocode.client.ui.iconButton +import ai.kilocode.client.util.UiTimerSource +import ai.kilocode.client.util.UiTimers import com.intellij.icons.AllIcons import com.intellij.ide.ui.LafManagerListener import com.intellij.openapi.application.ApplicationManager @@ -49,7 +51,6 @@ import javax.swing.JList import javax.swing.KeyStroke import javax.swing.ListSelectionModel import javax.swing.SwingUtilities -import javax.swing.Timer import javax.swing.event.DocumentEvent import javax.swing.event.ListDataEvent import javax.swing.event.ListDataListener @@ -59,6 +60,7 @@ class HistoryPanel( private val controller: HistoryController, private val nav: () -> Unit = {}, private val manager: SessionManager? = null, + private val timers: UiTimerSource = UiTimers, ) : BorderLayoutPanel(), Disposable, DataProvider { private val localSearch = search(controller.local) private val cloudSearch = search(controller.cloud) @@ -83,7 +85,7 @@ class HistoryPanel( .setText(KiloBundle.message("history.tab.cloud")) .setForeSideComponent(back()) private var stale = false - private val timer = Timer(ACTIVITY_MS) { syncActivity() } + private val timer = timers.timer(ACTIVITY_MS) { syncActivity() } private val tabs: JBTabs = JBTabsFactory.createTabs(null, this).apply { presentation.setSingleRow(true) presentation.setTabsPosition(JBTabsPosition.top) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionPanel.kt index 29ad9df649f..f277884c261 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionPanel.kt @@ -12,6 +12,8 @@ import ai.kilocode.client.ui.layout.HAlign import ai.kilocode.client.ui.layout.Stack import ai.kilocode.client.ui.layout.VAlign import ai.kilocode.client.ui.layout.align +import ai.kilocode.client.util.UiTimerSource +import ai.kilocode.client.util.UiTimers import ai.kilocode.rpc.dto.SessionDto import com.intellij.icons.AllIcons import com.intellij.ide.BrowserUtil @@ -37,7 +39,6 @@ import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.JButton import javax.swing.JComponent -import javax.swing.Timer /** * Empty-session panel. @@ -53,10 +54,11 @@ class EmptySessionPanel( private val activity: () -> Map = { emptyMap() }, private val titles: () -> Map = { emptyMap() }, private val browse: (String) -> Unit = BrowserUtil::browse, + private val timers: UiTimerSource = UiTimers, ) : BorderLayoutPanel(), Disposable, SessionEditorStyleTarget { val view: Align = align(HAlign.CENTER, VAlign.CENTER) - private val timer = Timer(ACTIVITY_MS) { syncActivity() } + private val timer = timers.timer(ACTIVITY_MS) { syncActivity() } internal val recent = RecentsList(recents, controller) private val historyButton = ShowHistoryButton().apply { @@ -180,7 +182,7 @@ class EmptySessionPanel( internal fun activeView() = getComponent(0) - internal fun text(session: SessionDto, now: Long = System.currentTimeMillis()) = + internal fun text(session: SessionDto, now: Long = timers.now()) = recent.text(session, now) internal fun rendererComponent( diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/auth/DeviceOAuthPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/auth/DeviceOAuthPanel.kt index a4add09ded5..4c1cd6c1d4b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/auth/DeviceOAuthPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/auth/DeviceOAuthPanel.kt @@ -4,6 +4,8 @@ import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.ui.HoverIcon import ai.kilocode.client.ui.RoundedContentPanel import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.util.UiTimerSource +import ai.kilocode.client.util.UiTimers import com.intellij.icons.AllIcons import com.intellij.openapi.ide.CopyPasteManager import com.intellij.openapi.ui.popup.Balloon @@ -29,7 +31,6 @@ import java.awt.event.MouseEvent import javax.swing.JButton import javax.swing.JPanel import javax.swing.SwingConstants -import javax.swing.Timer internal data class DeviceOAuthInfo( val url: String, @@ -48,6 +49,7 @@ internal class DeviceOAuthPanel( private val cancel: () -> Unit, private val browse: (String) -> Unit, private val prefix: String, + private val timers: UiTimerSource = UiTimers, ) : JPanel(GridBagLayout()) { val urlField = JBTextField().apply { isEditable = false @@ -100,7 +102,7 @@ internal class DeviceOAuthPanel( private var started = 0L private var expires = 900 private var last: String? = null - private val timer = Timer(1000) { syncTime() } + private val timer = timers.timer(1000) { syncTime() } init { border = JBUI.Borders.empty(UiStyle.Gap.pad()) @@ -182,7 +184,7 @@ internal class DeviceOAuthPanel( @RequiresEdt private fun syncTime() { - val elapsed = ((System.currentTimeMillis() - started) / 1000).toInt() + val elapsed = ((timers.now() - started) / 1000).toInt() val remain = (expires - elapsed).coerceAtLeast(0) val min = remain / 60 val sec = remain % 60 diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedOutProfileUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedOutProfileUi.kt index ceb42001a54..6324247efa3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedOutProfileUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedOutProfileUi.kt @@ -5,6 +5,8 @@ import ai.kilocode.client.settings.auth.DeviceOAuthInfo import ai.kilocode.client.settings.auth.DeviceOAuthPanel import ai.kilocode.client.settings.auth.DeviceOAuthText import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.util.UiTimerSource +import ai.kilocode.client.util.UiTimers import ai.kilocode.rpc.dto.KiloAppStatusDto import com.intellij.openapi.util.IconLoader import com.intellij.ui.components.JBLabel @@ -33,6 +35,7 @@ internal class LoggedOutProfileUi( private val retry: () -> Unit, private val cancel: () -> Unit, private val browse: (String) -> Unit, + private val timers: UiTimerSource = UiTimers, ) : JPanel(BorderLayout()) { private val cards = JPanel(CardLayout()) @@ -60,6 +63,7 @@ internal class LoggedOutProfileUi( cancel = cancel, browse = browse, prefix = "kilo.login", + timers = timers, ) private val initiatingIcon = AsyncProcessIcon("KiloInitiating").also { it.suspend() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt index 110a695264c..096f57fdbd7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt @@ -3,6 +3,8 @@ package ai.kilocode.client.settings.profile import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.telemetry.Telemetry +import ai.kilocode.client.util.UiTimerSource +import ai.kilocode.client.util.UiTimers import ai.kilocode.rpc.dto.KiloAppStateDto import ai.kilocode.rpc.dto.KiloAppStatusDto import ai.kilocode.rpc.dto.ProfileDto @@ -42,6 +44,7 @@ internal class ProfileUi( private val cs: CoroutineScope, private val app: KiloAppService = service(), private val browse: (String) -> Unit = { BrowserUtil.browse(it) }, + private val timers: UiTimerSource = UiTimers, ) : JPanel(BorderLayout()) { private val cards = JPanel(CardLayout()) @@ -52,6 +55,7 @@ internal class ProfileUi( retry = { app.retryAsync() }, cancel = ::cancel, browse = browse, + timers = timers, ) private val account = LoggedInProfileUi( dashboard = { @@ -191,7 +195,7 @@ internal class ProfileUi( val next = app.startLogin() withContext(edt) { if (id != attempt) return@withContext - login = LoginState.Pending(next, System.currentTimeMillis()) + login = LoginState.Pending(next, timers.now()) sync() browse(next.verificationUrl) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/util/UiTimers.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/util/UiTimers.kt new file mode 100644 index 00000000000..88e4b0074e8 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/util/UiTimers.kt @@ -0,0 +1,41 @@ +package ai.kilocode.client.util + +import javax.swing.Timer + +interface UiTimer { + fun start() + fun stop() + fun restart() + fun isRunning(): Boolean +} + +interface UiTimerSource { + fun now(): Long + fun timer(ms: Int, repeats: Boolean = true, action: () -> Unit): UiTimer +} + +object UiTimers : UiTimerSource { + override fun now(): Long = System.currentTimeMillis() + + override fun timer(ms: Int, repeats: Boolean, action: () -> Unit): UiTimer { + val timer = Timer(ms.coerceAtLeast(0)) { action() } + timer.isRepeats = repeats + return SwingUiTimer(timer) + } + + private class SwingUiTimer(private val timer: Timer) : UiTimer { + override fun start() { + timer.start() + } + + override fun stop() { + timer.stop() + } + + override fun restart() { + timer.restart() + } + + override fun isRunning(): Boolean = timer.isRunning + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt index 68c9201a484..0ff015a00d2 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt @@ -16,6 +16,7 @@ import ai.kilocode.client.session.model.QuestionItem import ai.kilocode.client.session.model.SessionState import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi +import ai.kilocode.client.testing.TestUiTimers import ai.kilocode.client.testing.FakeWorkspaceRpcApi import ai.kilocode.rpc.dto.ChatEventDto import ai.kilocode.rpc.dto.CloudSessionDto @@ -39,11 +40,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableSharedFlow -import java.awt.event.ActionEvent import javax.swing.JLabel import javax.swing.JComponent import javax.swing.JPanel -import javax.swing.Timer @Suppress("UnstableApiUsage") class SessionSidePanelManagerTest : BasePlatformTestCase() { @@ -53,6 +52,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { private lateinit var workspace: Workspace private lateinit var sessions: KiloSessionService private lateinit var app: KiloAppService + private lateinit var timers: TestUiTimers private val managers = mutableListOf() private val created = mutableListOf>() private val refs = mutableListOf() @@ -60,6 +60,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { override fun setUp() { super.setUp() + timers = TestUiTimers() scope = CoroutineScope(SupervisorJob()) rpc = FakeSessionRpcApi() sessions = KiloSessionService(project, scope, rpc) @@ -223,7 +224,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { manager.openSession(session("ses_1")) val first = active(manager) manager.openSession(session("ses_2")) - expire(manager, first) + expire() assertFalse(ui.contains(first)) assertEquals(listOf("/test" to "ses_1", "/test" to "ses_2"), created) @@ -236,7 +237,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { manager.openSession(session("ses_1")) val first = active(manager) manager.openSession(session("ses_2")) - expire(manager, first) + expire() manager.openSession(session("ses_1")) assertNotSame(first, active(manager)) @@ -257,7 +258,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { flow.emit(ChatEventDto.MessageUpdated("ses_1", msg("msg_hidden", "ses_1", "assistant"))) flow.emit(ChatEventDto.PartDelta("ses_1", "msg_hidden", "txt_hidden", "text", "stale")) } - expire(manager, first) + expire() manager.openSession(session("ses_1")) val second = active(manager) settle() @@ -590,7 +591,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { first.controller().model.setState(SessionState.AwaitingPermission(permission("ses_1"))) } manager.openSession(session("ses_2")) - expire(manager, first) + expire() assertFalse(ui.contains(first)) assertEquals(emptyMap(), manager.activity()) @@ -605,7 +606,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { first.controller().model.setState(SessionState.Busy("running")) } manager.openSession(session("ses_2")) - expire(manager, first) + expire() assertFalse(ui.contains(first)) } @@ -616,7 +617,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { manager.openSession(session("ses_1")) val first = active(manager) manager.openSession(session("ses_2")) - expire(manager, first) + expire() assertFalse(ui.contains(first)) assertEquals(emptyMap(), manager.activity()) @@ -628,7 +629,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { val manager = SessionSidePanelManager( project = project, root = workspace, - create = { project, workspace, owner, ref -> + create = { project, workspace, owner, ref, timers -> val id = when (ref) { is SessionRef.Local -> ref.id is SessionRef.Cloud -> ref.key @@ -636,7 +637,17 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { } created.add(workspace.directory to id) refs.add(ref) - SessionUi(project, workspace, sessions, app, scope, ref = ref, manager = owner, workspaces = workspaces).also { + SessionUi( + project, + workspace, + sessions, + app, + scope, + ref = ref, + manager = owner, + workspaces = workspaces, + timers = timers, + ).also { ui.add(it) Disposer.register(it) { ui.remove(it) } } @@ -644,6 +655,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { resolve = { workspaces.workspace(it) }, status = { sessions.activity() }, history = history, + timers = timers, ) managers.add(manager) return manager @@ -700,14 +712,8 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { method.invoke(manager) } - private fun expire(manager: SessionSidePanelManager, ui: JPanel) { - val field = SessionSidePanelManager::class.java.getDeclaredField("timers") - field.isAccessible = true - @Suppress("UNCHECKED_CAST") - val timers = field.get(manager) as Map - val timer = requireNotNull(timers[ui]) { "Expected an inactive session disposal timer" } - timer.stop() - timer.actionListeners.single().actionPerformed(ActionEvent(timer, ActionEvent.ACTION_PERFORMED, "expire")) + private fun expire() { + timers.advanceBy(10) } private fun settle() = kotlinx.coroutines.runBlocking { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt index 4fe55cf5d26..de7051b9222 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt @@ -12,6 +12,7 @@ import ai.kilocode.client.session.controller.SessionController import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi import ai.kilocode.client.testing.FakeWorkspaceRpcApi +import ai.kilocode.client.testing.TestCoroutines import ai.kilocode.client.session.SessionRef import ai.kilocode.client.session.scroll.SessionScroll import ai.kilocode.rpc.dto.ChatEventDto @@ -29,8 +30,6 @@ import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.openapi.util.Disposer import com.intellij.util.ui.UIUtil import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import java.awt.Container @@ -42,6 +41,7 @@ import javax.swing.JScrollBar @Suppress("UnstableApiUsage") abstract class SessionUiTestBase : BasePlatformTestCase() { + private lateinit var coroutines: TestCoroutines protected lateinit var scope: CoroutineScope protected lateinit var sessions: KiloSessionService protected lateinit var app: KiloAppService @@ -53,7 +53,8 @@ abstract class SessionUiTestBase : BasePlatformTestCase() { override fun setUp() { super.setUp() - scope = CoroutineScope(SupervisorJob()) + coroutines = TestCoroutines() + scope = coroutines.scope rpc = FakeSessionRpcApi() appRpc = FakeAppRpcApi().also { @@ -75,7 +76,7 @@ abstract class SessionUiTestBase : BasePlatformTestCase() { override fun tearDown() { try { Disposer.dispose(ui) - scope.cancel() + coroutines.close { UIUtil.dispatchAllInvocationEvents() } } finally { super.tearDown() } @@ -116,11 +117,8 @@ abstract class SessionUiTestBase : BasePlatformTestCase() { (scrollView() as? Container)?.doLayout() } - protected fun settle() = runBlocking { - repeat(5) { - delay(100) - UIUtil.dispatchAllInvocationEvents() - } + protected fun settle() { + coroutines.drain { UIUtil.dispatchAllInvocationEvents() } } protected fun settleShort(ms: Long) = runBlocking { @@ -141,7 +139,7 @@ abstract class SessionUiTestBase : BasePlatformTestCase() { emit(ChatEventDto.MessageUpdated("ses_test", message(id)), flush = false) emit(ChatEventDto.PartUpdated("ses_test", part("part_$i", id, "text", text(i))), flush = false) } - settleShort(100) + settle() forceFlush() drainScroll() } @@ -149,7 +147,7 @@ abstract class SessionUiTestBase : BasePlatformTestCase() { protected fun emit(event: ChatEventDto, flush: Boolean = true) { runBlocking { rpc.events.emit(event) } if (flush) { - settleShort(20) + settle() forceFlush() } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/DelayedStateTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/DelayedStateTest.kt index 349171f6549..7b060ef0daf 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/DelayedStateTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/DelayedStateTest.kt @@ -1,14 +1,18 @@ package ai.kilocode.client.session.controller +import ai.kilocode.client.testing.TestUiTimers import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.util.Disposer import com.intellij.testFramework.fixtures.BasePlatformTestCase -import com.intellij.util.ui.UIUtil -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking class DelayedStateTest : BasePlatformTestCase() { private val states = mutableListOf() + private lateinit var timers: TestUiTimers + + override fun setUp() { + super.setUp() + timers = TestUiTimers() + } override fun tearDown() { try { @@ -25,7 +29,7 @@ class DelayedStateTest : BasePlatformTestCase() { val delay = delayed(30) delay.run("loading", { state }) { out.add(it) } - pause(120) + timers.advanceBy(30) assertEquals(listOf("loading"), out) } @@ -37,7 +41,7 @@ class DelayedStateTest : BasePlatformTestCase() { delay.run("loading", { state }) { out.add(it) } edt { state = "loaded" } - pause(120) + timers.advanceBy(30) assertTrue(out.isEmpty()) } @@ -50,7 +54,7 @@ class DelayedStateTest : BasePlatformTestCase() { delay.run("loading", { first }) { out.add("first:$it") } delay.run("connecting", { second }) { out.add("second:$it") } - pause(120) + timers.advanceBy(30) assertEquals(listOf("first:loading", "second:connecting"), out) } @@ -63,7 +67,7 @@ class DelayedStateTest : BasePlatformTestCase() { delay.run("first", { state }) { out.add(it) } edt { state = "second" } delay.run("second", { state }) { out.add(it) } - pause(120) + timers.advanceBy(30) assertEquals(listOf("second"), out) } @@ -75,7 +79,7 @@ class DelayedStateTest : BasePlatformTestCase() { delay.run("loading", { state }) { out.add(it) } delay.cancel() - pause(120) + timers.advanceBy(30) assertTrue(out.isEmpty()) } @@ -85,9 +89,9 @@ class DelayedStateTest : BasePlatformTestCase() { val delay = delayed(30) delay.run("loading", { state }) {} - pause(10) + timers.advanceBy(10) delay.cancel() - pause(10) + timers.advanceBy(10) assertFalse(delay.active()) } @@ -97,7 +101,7 @@ class DelayedStateTest : BasePlatformTestCase() { val delay = delayed(30) delay.run("loading", { state }) {} - pause(120) + timers.advanceBy(30) assertFalse(delay.active()) } @@ -110,7 +114,7 @@ class DelayedStateTest : BasePlatformTestCase() { delay.run("loading", { state }) { out.add(it) } Disposer.dispose(delay) states.remove(delay) - pause(120) + timers.advanceBy(30) assertTrue(out.isEmpty()) } @@ -123,25 +127,17 @@ class DelayedStateTest : BasePlatformTestCase() { delay.run("loading", { state }) { out.add(ApplicationManager.getApplication().isDispatchThread) } - pause(30) + timers.advanceBy(0) assertEquals(listOf(true), out) } private fun delayed(ms: Long): DelayedState { - val state = DelayedState(ms) + val state = DelayedState(ms, timers) states.add(state) return state } - private fun pause(ms: Long) = runBlocking { - val tick = 10L - repeat((ms / tick).coerceAtLeast(1).toInt()) { - delay(tick) - edt { UIUtil.dispatchAllInvocationEvents() } - } - } - private fun edt(block: () -> Unit) { ApplicationManager.getApplication().invokeAndWait(block) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt index 423a13ea49a..2cc6d6fadd2 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt @@ -8,6 +8,8 @@ import ai.kilocode.client.session.model.SessionState import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.FakeWorkspaceRpcApi import ai.kilocode.client.testing.FakeSessionRpcApi +import ai.kilocode.client.testing.TestCoroutines +import ai.kilocode.client.testing.TestUiTimers import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace import ai.kilocode.client.session.SessionRef @@ -35,8 +37,6 @@ import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.util.ui.UIUtil import java.awt.event.HierarchyEvent import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking @@ -95,7 +95,9 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() { protected lateinit var app: KiloAppService protected lateinit var workspaces: KiloWorkspaceService protected lateinit var workspace: Workspace + protected lateinit var timers: TestUiTimers + private lateinit var coroutines: TestCoroutines protected lateinit var scope: CoroutineScope protected lateinit var parent: Disposable @@ -104,8 +106,10 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() { rpc = FakeSessionRpcApi() appRpc = FakeAppRpcApi() projectRpc = FakeWorkspaceRpcApi() + timers = TestUiTimers() - scope = CoroutineScope(SupervisorJob()) + coroutines = TestCoroutines() + scope = coroutines.scope parent = Disposer.newDisposable("test") sessions = KiloSessionService(project, scope, rpc) @@ -117,7 +121,7 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() { override fun tearDown() { try { Disposer.dispose(parent) - scope.cancel() + coroutines.close { edt { UIUtil.dispatchAllInvocationEvents() } } } finally { super.tearDown() } @@ -169,6 +173,7 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() { beforeUpdate = beforeUpdate, afterUpdate = afterUpdate, telemetry = { event, props -> appRpc.telemetry.add(TelemetryCaptureDto(event, props)) }, + timers = timers, ) controllers.add(m) roots[m] = root @@ -222,33 +227,38 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() { // ------ EDT + coroutine helpers ------ - /** Let coroutines settle without forcing buffered controller delivery. */ - protected fun settle() = runBlocking { - repeat(5) { - delay(100) - edt { UIUtil.dispatchAllInvocationEvents() } - } + /** Drain background coroutine and EDT work without forcing buffered controller delivery. */ + protected fun settle() { + drain(false) } - /** Let coroutines settle, force buffered controller delivery, then drain EDT. */ - protected fun flush() = runBlocking { - repeat(5) { - delay(100) - edt { - controllers.forEach { it.flushEvents() } - UIUtil.dispatchAllInvocationEvents() - } - } + /** Drain background work, force buffered controller delivery, then drain EDT. */ + protected fun flush() { + drain(true) } protected fun pause(ms: Long) = runBlocking { - val tick = 10L - repeat((ms / tick).coerceAtLeast(1).toInt()) { - delay(tick) + settleFast() + timers.advanceBy(ms) + settleFast() + } + + private suspend fun settleFast() { + repeat(3) { + delay(1) edt { UIUtil.dispatchAllInvocationEvents() } } } + private fun drain(force: Boolean) { + coroutines.drain { + edt { + if (force) controllers.forEach { it.flushEvents() } + UIUtil.dispatchAllInvocationEvents() + } + } + } + protected fun edt(block: () -> Unit) { ApplicationManager.getApplication().invokeAndWait(block) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionUpdateQueueTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionUpdateQueueTest.kt index 1f08b695574..38115406e46 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionUpdateQueueTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionUpdateQueueTest.kt @@ -404,11 +404,10 @@ class SessionUpdateQueueTest : SessionControllerTestBase() { val sb = StringBuilder() for (token in tokens) { sb.append(token) - emit(ChatEventDto.PartUpdated("ses_test", part("prt1", "ses_test", "msg1", "text", text = sb.toString()))) - emit(ChatEventDto.PartDelta("ses_test", "msg1", "prt1", "text", token)) + emit(ChatEventDto.PartUpdated("ses_test", part("prt1", "ses_test", "msg1", "text", text = sb.toString())), flush = false) + emit(ChatEventDto.PartDelta("ses_test", "msg1", "prt1", "text", token), flush = false) + flush() } - settle() - flush() val text = (m.model.message("msg1")!!.parts["prt1"] as ai.kilocode.client.session.model.Text).content.toString() assertEquals(sb.toString(), text) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt index 2709b6bbbe1..a0402fb353f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanelTest.kt @@ -433,8 +433,9 @@ class SessionHeaderPanelTest : SessionControllerTestBase() { val c = promptedHeader() val panel = SessionHeaderPanel(c, parent) repeat(12) { idx -> - emit(ChatEventDto.PartUpdated("ses_test", tool("tool_more_$idx", "bash", "running", "More $idx"))) + emit(ChatEventDto.PartUpdated("ses_test", tool("tool_more_$idx", "bash", "running", "More $idx")), flush = false) } + flush() panel.timelineViewport().setSize(panel.timelineBarWidth() * 4, panel.timelineViewportPreferredSize().height) panel.timelineViewport().doLayout() panel.timelineViewport().viewPosition = Point(0, 0) @@ -471,8 +472,9 @@ class SessionHeaderPanelTest : SessionControllerTestBase() { val c = promptedHeader() val panel = SessionHeaderPanel(c, parent) repeat(12) { idx -> - emit(ChatEventDto.PartUpdated("ses_test", tool("tool_touch_$idx", "bash", "running", "Touch $idx"))) + emit(ChatEventDto.PartUpdated("ses_test", tool("tool_touch_$idx", "bash", "running", "Touch $idx")), flush = false) } + flush() panel.timelineViewport().setSize(panel.timelineBarWidth() * 4, panel.timelineViewportPreferredSize().height) panel.timelineViewport().doLayout() panel.timelineViewport().viewPosition = Point(0, 0) @@ -505,8 +507,9 @@ class SessionHeaderPanelTest : SessionControllerTestBase() { val c = promptedHeader() val panel = SessionHeaderPanel(c, parent) repeat(12) { idx -> - emit(ChatEventDto.PartUpdated("ses_test", tool("tool_wheel_$idx", "bash", "running", "Wheel $idx"))) + emit(ChatEventDto.PartUpdated("ses_test", tool("tool_wheel_$idx", "bash", "running", "Wheel $idx")), flush = false) } + flush() panel.timelineViewport().setSize(panel.timelineBarWidth() * 4, panel.timelineViewportPreferredSize().height) panel.timelineViewport().doLayout() panel.timelineViewport().viewPosition = Point(0, 0) @@ -539,8 +542,9 @@ class SessionHeaderPanelTest : SessionControllerTestBase() { val c = promptedHeader() val panel = SessionHeaderPanel(c, parent) repeat(12) { idx -> - emit(ChatEventDto.PartUpdated("ses_test", tool("tool_more_$idx", "bash", "running", "More $idx"))) + emit(ChatEventDto.PartUpdated("ses_test", tool("tool_more_$idx", "bash", "running", "More $idx")), flush = false) } + flush() panel.timelineViewport().setSize(panel.timelineBarWidth() * 4, panel.timelineViewportPreferredSize().height) panel.timelineViewport().doLayout() panel.timelineViewport().viewPosition = Point(0, 0) @@ -580,16 +584,17 @@ class SessionHeaderPanelTest : SessionControllerTestBase() { edt { c.prompt("go") } flush() - emit(ChatEventDto.SessionUpdated("ses_test", session("ses_test", title = "Generated title"))) - emit(ChatEventDto.MessageUpdated("ses_test", assistant())) - emit(ChatEventDto.PartUpdated("ses_test", reasoning(done = false, text = "Thinking"))) - emit(ChatEventDto.PartUpdated("ses_test", tool("tool_1", "bash", "running", "Run tests", input = mapOf("cmd" to "test", "files" to "src")))) - emit(ChatEventDto.PartUpdated("ses_test", tool("tool_2", "edit", "error", "Edit file", input = mapOf("cmd" to "test", "files" to "src")))) - emit(ChatEventDto.PartUpdated("ses_test", stepFinish())) + emit(ChatEventDto.SessionUpdated("ses_test", session("ses_test", title = "Generated title")), flush = false) + emit(ChatEventDto.MessageUpdated("ses_test", assistant()), flush = false) + emit(ChatEventDto.PartUpdated("ses_test", reasoning(done = false, text = "Thinking")), flush = false) + emit(ChatEventDto.PartUpdated("ses_test", tool("tool_1", "bash", "running", "Run tests", input = mapOf("cmd" to "test", "files" to "src"))), flush = false) + emit(ChatEventDto.PartUpdated("ses_test", tool("tool_2", "edit", "error", "Edit file", input = mapOf("cmd" to "test", "files" to "src"))), flush = false) + emit(ChatEventDto.PartUpdated("ses_test", stepFinish()), flush = false) emit(ChatEventDto.TodoUpdated("ses_test", listOf( TodoDto("Write tests", "completed", "high"), TodoDto("Ship it", "pending", "medium"), - ))) + )), flush = false) + flush() return c } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/TestCoroutines.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/TestCoroutines.kt new file mode 100644 index 00000000000..03c735585e1 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/TestCoroutines.kt @@ -0,0 +1,39 @@ +package ai.kilocode.client.testing + +import java.util.concurrent.Executors +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch + +class TestCoroutines { + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val job = SupervisorJob() + + val scope = CoroutineScope(job + dispatcher) + + fun drain(pump: () -> Unit) { + repeat(5) { + await(scope.launch {}, pump) + pump() + } + } + + fun close(pump: () -> Unit) { + job.cancel() + try { + await(job, pump) + } finally { + dispatcher.close() + } + } + + private fun await(job: kotlinx.coroutines.Job, pump: () -> Unit) { + val end = System.nanoTime() + 5_000_000_000L + while (!job.isCompleted) { + check(System.nanoTime() < end) { "Timed out draining test coroutines" } + pump() + Thread.yield() + } + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/TestUiTimers.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/TestUiTimers.kt new file mode 100644 index 00000000000..13c6d7ede54 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/TestUiTimers.kt @@ -0,0 +1,81 @@ +package ai.kilocode.client.testing + +import ai.kilocode.client.util.UiTimer +import ai.kilocode.client.util.UiTimerSource +import com.intellij.openapi.application.ApplicationManager + +class TestUiTimers : UiTimerSource { + private val timers = linkedSetOf() + private var time = 0L + + override fun now(): Long = time + + override fun timer(ms: Int, repeats: Boolean, action: () -> Unit): UiTimer { + return TestTimer(ms.coerceAtLeast(0), repeats, action) + } + + fun advanceBy(ms: Long) { + edt { } + time += ms.coerceAtLeast(0) + runDue() + } + + fun runDue() { + while (true) { + val due = timers.filter { it.running && it.due <= time } + if (due.isEmpty()) return + due.forEach { it.fire() } + } + } + + private fun edt(action: () -> Unit) { + val app = ApplicationManager.getApplication() + if (app.isDispatchThread) { + action() + return + } + app.invokeAndWait(action) + } + + private inner class TestTimer( + private val ms: Int, + private val repeats: Boolean, + private val action: () -> Unit, + ) : UiTimer { + var running = false + private set + var due = Long.MAX_VALUE + private set + + override fun start() { + if (running) return + running = true + due = time + ms + timers.add(this) + } + + override fun stop() { + running = false + due = Long.MAX_VALUE + timers.remove(this) + } + + override fun restart() { + running = true + due = time + ms + timers.add(this) + } + + override fun isRunning(): Boolean = running + + fun fire() { + if (!running || due > time) return + if (repeats) { + due = time + ms.coerceAtLeast(1) + } else { + stop() + } + edt(action) + } + } +} diff --git a/packages/kilo-ui/src/components/diff.tsx b/packages/kilo-ui/src/components/diff.tsx index 87db2bb4a70..947cb2b4dc3 100644 --- a/packages/kilo-ui/src/components/diff.tsx +++ b/packages/kilo-ui/src/components/diff.tsx @@ -223,7 +223,7 @@ export function Diff(props: DiffProps) { } const perf = large() ? { ...base, ...largeOptions } : base - if (!mobile()) return perf + if (!mobile() || props.disableLineNumbers === false) return perf return { ...perf, diff --git a/packages/kilo-ui/src/pierre/index.test.ts b/packages/kilo-ui/src/pierre/index.test.ts new file mode 100644 index 00000000000..5f2df0e9518 --- /dev/null +++ b/packages/kilo-ui/src/pierre/index.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, test } from "bun:test" +import { createDefaultOptions } from "./index" + +describe("Pierre diff options", () => { + test("highlights changed characters in unified and split diffs", () => { + expect(createDefaultOptions("unified").lineDiffType).toBe("char") + expect(createDefaultOptions("split").lineDiffType).toBe("char") + }) +}) diff --git a/packages/kilo-ui/src/pierre/index.ts b/packages/kilo-ui/src/pierre/index.ts index 189a3147bf1..1df03fb88d2 100644 --- a/packages/kilo-ui/src/pierre/index.ts +++ b/packages/kilo-ui/src/pierre/index.ts @@ -10,6 +10,8 @@ import { createDefaultOptions as defaults, styleVariables } from "@opencode-ai/u export { styleVariables } +export const LINE_DIFF_TYPE = "char" as const + // Pierre 1.1 treats its changed-line override properties as tint targets. Apply // Kilo semantic surfaces at the computed row level so host diff colors stay final. const css = ` @@ -35,6 +37,7 @@ export function createDefaultOptions(style: FileDiffOptions["diffStyle"]) const opts = defaults(style) return { ...opts, + lineDiffType: LINE_DIFF_TYPE, unsafeCSS: `${opts.unsafeCSS}\n${css}`, } } diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 876b6f9e9f4..6dc6ac666be 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -235,6 +235,11 @@ "title": "Agent Manager: Next Tab", "category": "Kilo Code" }, + { + "command": "kilo-code.new.agentManager.search", + "title": "Agent Manager: Search Worktrees and Sessions", + "category": "Kilo Code" + }, { "command": "kilo-code.new.agentManager.showTerminal", "title": "Agent Manager: Focus Terminal", @@ -629,6 +634,12 @@ "mac": "cmd+alt+right", "when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'" }, + { + "command": "kilo-code.new.agentManager.search", + "key": "ctrl+f", + "mac": "cmd+f", + "when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && !terminalFocus" + }, { "command": "kilo-code.new.agentManager.showTerminal", "key": "ctrl+/", @@ -902,10 +913,6 @@ "default": false, "description": "Enable chat textarea autocomplete" }, - "kilo-code.new.speechToText.model": { - "type": "string", - "description": "Model to use for experimental speech-to-text voice input. Requires Kilo Gateway." - }, "kilo-code.new.claudeCodeCompat": { "type": "boolean", "default": false, @@ -1111,7 +1118,6 @@ "@xterm/addon-web-links": "0.12.0", "@xterm/xterm": "6.0.0", "diff": "8.0.4", - "dotenv": "^16.4.7", "fastest-levenshtein": "^1.0.16", "friendly-words": "1.3.1", "ignore": "^7.0.3", diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 0426e3c9de6..9ed1cbadd14 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -301,7 +301,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private configWarningsShown = false /** Cached notificationsLoaded payload */ private cachedNotificationsMessage: unknown = null - private pendingKiloModelID: string | null = null + private pendingKiloModel: { modelID?: string; agent?: string } | null = null private pendingReviewComments: { comments: unknown[]; autoSend: boolean }[] = [] private readyResolvers: (() => void)[] = [] private promptRecoveryQueued = false @@ -716,8 +716,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.postMessage({ type: "openCloudSession", sessionId }) } - public selectKiloModel(modelID: string): void { - this.pendingKiloModelID = modelID + public selectKiloModel(modelID?: string, agent?: string): void { + if (!modelID && !agent) return + this.pendingKiloModel = { ...(modelID && { modelID }), ...(agent && { agent }) } this.flushPendingKiloModel() } @@ -3292,11 +3293,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } private flushPendingKiloModel(): void { - if (!this.webview || !this.isWebviewReady || !this.client || !this.pendingKiloModelID) return + if (!this.webview || !this.isWebviewReady || !this.client || !this.pendingKiloModel) return - const modelID = this.pendingKiloModelID - this.pendingKiloModelID = null - this.postMessage({ type: "selectKiloModel", modelID }) + const pending = this.pendingKiloModel + this.pendingKiloModel = null + this.postMessage({ type: "selectKiloModel", ...pending }) } public async appendReviewComments(comments: unknown[], autoSend = false): Promise { diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index 72cfd6d4cd1..096a4514ac3 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -1746,6 +1746,18 @@ export class AgentManagerProvider implements Disposable { getClient: () => this.connectionService.getClient(), createWorktreeOnDisk: (opts) => this.createWorktreeOnDisk(opts), runSetupScript: (p, b, id) => this.runSetupScriptForWorktree(p, b, id), + cleanupWorktree: async (id) => { + await this.onDeleteWorktree(id) + }, + notifyError: (error, result, id) => { + this.postToWebview({ + type: "agentManager.worktreeSetup", + status: "error", + message: error, + branch: result.branch, + worktreeId: id, + }) + }, getStateManager: () => this.getStateManager(), registerWorktreeSession: (sid, dir) => this.registerWorktreeSession(sid, dir), registerSession: (session) => this.panel?.sessions.registerSession(session), diff --git a/packages/kilo-vscode/src/agent-manager/GitOps.ts b/packages/kilo-vscode/src/agent-manager/GitOps.ts index c490a576411..92a2984b5a9 100644 --- a/packages/kilo-vscode/src/agent-manager/GitOps.ts +++ b/packages/kilo-vscode/src/agent-manager/GitOps.ts @@ -48,6 +48,12 @@ export interface ExecResult { stderr: string } +export interface ExecBufferResult { + code: number + stdout: Buffer + stderr: string +} + /** * Fixed SSH command injected by {@link nonInteractiveEnv} when the user has * not already configured their own. Exported so callers can check whether a @@ -532,12 +538,21 @@ export class GitOps { return this.exec(args, cwd, options) } - private exec(args: string[], cwd: string, options?: ExecOptions): Promise { + execGitBuffer(args: string[], cwd: string): Promise { + return this.execBuffer(args, cwd) + } + + private async exec(args: string[], cwd: string, options?: ExecOptions): Promise { + const result = await this.execBuffer(args, cwd, options) + return { code: result.code, stdout: result.stdout.toString("utf8"), stderr: result.stderr } + } + + private execBuffer(args: string[], cwd: string, options?: ExecOptions): Promise { if (this.controller.signal.aborted) { - return Promise.resolve({ code: 1, stdout: "", stderr: "GitOps disposed" }) + return Promise.resolve({ code: 1, stdout: Buffer.alloc(0), stderr: "GitOps disposed" }) } const invoke = () => - new Promise((resolve) => { + new Promise((resolve) => { const child = spawn("git", args, { cwd, env: options?.env, @@ -547,7 +562,7 @@ export class GitOps { if (options?.stdin !== undefined) { if (!child.stdin) { - resolve({ code: 1, stdout: "", stderr: "stdin not available for git process" }) + resolve({ code: 1, stdout: Buffer.alloc(0), stderr: "stdin not available for git process" }) return } child.stdin.end(options.stdin) @@ -559,12 +574,12 @@ export class GitOps { child.stderr?.on("data", (chunk: Buffer) => err.push(chunk)) child.on("error", (error) => { - resolve({ code: 1, stdout: "", stderr: error.message }) + resolve({ code: 1, stdout: Buffer.alloc(0), stderr: error.message }) }) child.on("close", (code) => { resolve({ code: code ?? 1, - stdout: Buffer.concat(out).toString("utf8"), + stdout: Buffer.concat(out), stderr: Buffer.concat(err).toString("utf8"), }) }) diff --git a/packages/kilo-vscode/src/agent-manager/continue-in-worktree.ts b/packages/kilo-vscode/src/agent-manager/continue-in-worktree.ts index cd49f2d2064..ef4fcdd9226 100644 --- a/packages/kilo-vscode/src/agent-manager/continue-in-worktree.ts +++ b/packages/kilo-vscode/src/agent-manager/continue-in-worktree.ts @@ -14,6 +14,8 @@ export interface ContinueContext { result: CreateWorktreeResult } | null> runSetupScript: (path: string, branch: string, worktreeId: string) => Promise + cleanupWorktree: (worktreeId: string) => Promise + notifyError: (error: string, result: CreateWorktreeResult, worktreeId: string) => void getStateManager: () => WorktreeStateManager | undefined registerWorktreeSession: (sessionId: string, directory: string) => void registerSession: (session: Session) => void @@ -72,6 +74,23 @@ export async function transferState( return { ok: true, value: undefined } } +async function rollback( + ctx: ContinueContext, + prepared: { worktreeId: string; result: CreateWorktreeResult }, + error: string, + progress: (status: string, detail?: string, error?: string) => void, +): Promise { + await ctx.cleanupWorktree(prepared.worktreeId).catch((err) => { + ctx.log("Failed to clean up worktree after continue error:", getErrorMessage(err)) + }) + try { + ctx.notifyError(error, prepared.result, prepared.worktreeId) + } catch (err) { + ctx.log("Failed to notify Agent Manager about continue error:", getErrorMessage(err)) + } + progress("error", undefined, error) +} + /** Fork the session into the worktree directory. */ export async function forkSession(ctx: ContinueContext, sessionId: string, dir: string): Promise> { let client: KiloClient @@ -136,7 +155,7 @@ export async function continueInWorktree( progress("transferring", "Transferring changes...") const transferred = await transferState(ctx, captured.value, prepared.value.result.path) - if (!transferred.ok) return progress("error", undefined, transferred.error) + if (!transferred.ok) return rollback(ctx, prepared.value, transferred.error, progress) progress("forking", "Starting session...") const forked = await forkSession(ctx, sessionId, prepared.value.result.path) diff --git a/packages/kilo-vscode/src/agent-manager/format-keybinding.ts b/packages/kilo-vscode/src/agent-manager/format-keybinding.ts index daa15a3af59..c6a4b05f150 100644 --- a/packages/kilo-vscode/src/agent-manager/format-keybinding.ts +++ b/packages/kilo-vscode/src/agent-manager/format-keybinding.ts @@ -67,6 +67,7 @@ export function buildKeybindingMap( // Ensure fallback bindings are always present (may be missing from // cached packageJSON if the extension hasn't been fully reloaded) + if (!bindings.search) bindings.search = formatKeybinding(mac ? "cmd+f" : "ctrl+f", mac) if (!bindings.runScript) bindings.runScript = formatKeybinding(mac ? "cmd+e" : "ctrl+e", mac) if (!bindings.toggleDiff) bindings.toggleDiff = formatKeybinding(mac ? "cmd+d" : "ctrl+d", mac) if (!bindings.showShortcuts) bindings.showShortcuts = formatKeybinding(mac ? "cmd+shift+/" : "ctrl+shift+/", mac) diff --git a/packages/kilo-vscode/src/agent-manager/local-diff.ts b/packages/kilo-vscode/src/agent-manager/local-diff.ts index 82ac3e2521b..e643e866270 100644 --- a/packages/kilo-vscode/src/agent-manager/local-diff.ts +++ b/packages/kilo-vscode/src/agent-manager/local-diff.ts @@ -1,6 +1,7 @@ import * as fs from "fs/promises" -import * as path from "path" import { binaryFile } from "../diff/shared/binary" +import { imageMime, loadImage, readImageFile } from "../diff/shared/image" +import { resolveInside } from "../diff/shared/path" import type { GitOps } from "./GitOps" import type { WorktreeDiffEntry } from "./types" @@ -139,16 +140,20 @@ async function numstat(git: GitOps, dir: string, base: string, file?: string) { } async function statStamp(dir: string, file: string): Promise { - const stat = await fs.stat(path.join(dir, file)).catch(() => undefined) + const full = resolveInside(dir, file) + if (!full) return `missing:${file}` + const stat = await fs.lstat(full).catch(() => undefined) if (!stat) return `missing:${file}` return `${stat.size}:${stat.mtimeMs}` } async function lineCount(file: string): Promise { - const stat = await fs.stat(file).catch(() => undefined) + const stat = await fs.lstat(file).catch(() => undefined) if (!stat || stat.size === 0) return 0 if (stat.size > MAX_UNTRACKED_BYTES) return 0 - const content = await fs.readFile(file, "utf-8").catch(() => "") + const content = stat.isSymbolicLink() + ? await fs.readlink(file).catch(() => "") + : await fs.readFile(file, "utf-8").catch(() => "") if (!content) return 0 if (content.endsWith("\n")) return content.split("\n").length - 1 return content.split("\n").length @@ -191,7 +196,8 @@ async function list(git: GitOps, dir: string, anc: string, log?: Log): Promise undefined) + const full = resolveInside(dir, file) + if (!full) continue + const exists = await fs.lstat(full).catch(() => undefined) if (!exists) continue const binary = await binaryFile(full) result.push({ @@ -226,6 +233,7 @@ async function list(git: GitOps, dir: string, anc: string, log?: Log): Promise => { const state = states.get(`${dir}\0${base}`) - const meta = state?.metas.get(file) - if (!state || !meta) return diffFile(git, dir, base, file, log) + if (!state) return diffFile(git, dir, base, file, log) + const meta = state.metas.get(file) + if (!meta) return null return materialize(git, dir, state.anc, meta, log) }, } } async function detailMeta(git: GitOps, dir: string, anc: string, file: string): Promise { + const full = resolveInside(dir, file) + if (!full) return undefined const tracked = await git.execGit(["ls-files", "--error-unmatch", "--", file], dir) if (tracked.code !== 0) { - const full = path.join(dir, file) - const exists = await fs.stat(full).catch(() => undefined) + const untracked = await git.execGit(["ls-files", "--others", "--exclude-standard", "--", file], dir) + if (untracked.code !== 0 || !untracked.stdout.split("\n").includes(file)) return undefined + const exists = await fs.lstat(full).catch(() => undefined) if (!exists) return undefined const binary = await binaryFile(full) return { @@ -323,7 +336,10 @@ async function detailMeta(git: GitOps, dir: string, anc: string, file: string): tracked: true, generatedLike: generatedLike(pathPart), binary: stat.binary, - stamp: status === "deleted" ? `deleted:${anc}` : await statStamp(dir, pathPart), + stamp: + status === "deleted" + ? `deleted:${anc}` + : `${imageMime(pathPart) ? `${anc}:` : ""}${await statStamp(dir, pathPart)}`, } } @@ -334,10 +350,25 @@ async function blobSize(git: GitOps, dir: string, anc: string, file: string): Pr } async function fileSize(dir: string, file: string): Promise { - const stat = await fs.stat(path.join(dir, file)).catch(() => undefined) + const full = resolveInside(dir, file) + if (!full) return 0 + const stat = await fs.lstat(full).catch(() => undefined) return stat?.size ?? 0 } +async function readBlob(git: GitOps, dir: string, ref: string, file: string): Promise { + const result = await git.execGitBuffer(["show", `${ref}:${file}`], dir) + return result.code === 0 ? result.stdout : undefined +} + +async function readFile(dir: string, file: string): Promise { + const full = resolveInside(dir, file) + if (!full) return undefined + const stat = await fs.lstat(full).catch(() => undefined) + if (!stat?.isFile()) return undefined + return readImageFile(full) +} + async function readBefore(git: GitOps, dir: string, anc: string, file: string, status: Status): Promise { if (status === "added") return "" const result = await git.execGit(["show", `${anc}:${file}`], dir) @@ -346,9 +377,12 @@ async function readBefore(git: GitOps, dir: string, anc: string, file: string, s async function readAfter(dir: string, file: string, status: Status): Promise { if (status === "deleted") return "" - const full = path.join(dir, file) - const exists = await fs.stat(full).catch(() => undefined) - if (!exists) return "" + const full = resolveInside(dir, file) + if (!full) return "" + const stat = await fs.lstat(full).catch(() => undefined) + if (!stat) return "" + if (stat.isSymbolicLink()) return fs.readlink(full).catch(() => "") + if (!stat.isFile()) return "" return fs.readFile(full, "utf-8").catch(() => "") } @@ -385,12 +419,21 @@ export async function diffFile( } async function materialize(git: GitOps, dir: string, anc: string, meta: Meta, log?: Log): Promise { - if (meta.binary) return summarize(meta) + const mime = imageMime(meta.file) + if (meta.binary && !mime) return summarize(meta) + const beforeBytes = meta.status === "added" ? 0 : await blobSize(git, dir, anc, meta.file) + const afterBytes = meta.status === "deleted" ? 0 : await fileSize(dir, meta.file) + if (mime) { + const image = await loadImage( + meta.file, + meta.status === "added" ? undefined : { bytes: beforeBytes, read: () => readBlob(git, dir, anc, meta.file) }, + meta.status === "deleted" ? undefined : { bytes: afterBytes, read: () => readFile(dir, meta.file) }, + ) + return { ...summarize(meta), summarized: false, image } + } // Cheap size probe before materializing content — protects the extension // host from OOM on huge tracked files. `git cat-file -s` returns the blob // size without streaming its contents, and `fs.stat` is a plain syscall. - const beforeBytes = meta.status === "added" ? 0 : await blobSize(git, dir, anc, meta.file) - const afterBytes = meta.status === "deleted" ? 0 : await fileSize(dir, meta.file) if (beforeBytes > MAX_DETAIL_BYTES || afterBytes > MAX_DETAIL_BYTES) { log?.("diffFile: file too large for detail view, returning summarized entry", { file: meta.file, diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 360b32c7217..ed766104333 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -8,6 +8,7 @@ */ import type { SnapshotFileDiff } from "@kilocode/sdk/v2/client" +import type { DiffImage } from "../diff/types" import type { Worktree, ManagedSession, Section } from "./WorktreeStateManager" import type { WorktreeStats, LocalStats } from "./GitStatsPoller" import type { ApplyConflict } from "./GitOps" @@ -33,6 +34,8 @@ export type WorktreeDiffEntry = SnapshotFileDiff & { generatedLike?: boolean summarized?: boolean stamp?: string + kind?: "image" + image?: DiffImage } // --------------------------------------------------------------------------- diff --git a/packages/kilo-vscode/src/diff/shared/image.ts b/packages/kilo-vscode/src/diff/shared/image.ts new file mode 100644 index 00000000000..6b3a887f152 --- /dev/null +++ b/packages/kilo-vscode/src/diff/shared/image.ts @@ -0,0 +1,69 @@ +import { createReadStream } from "fs" +import * as path from "path" +import type { DiffImage, DiffImageSide } from "../types" + +const MIMES: Record = { + ".avif": "image/avif", + ".bmp": "image/bmp", + ".gif": "image/gif", + ".ico": "image/x-icon", + ".jpe": "image/jpeg", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".png": "image/png", + ".svg": "image/svg+xml", + ".webp": "image/webp", +} + +export const MAX_IMAGE_BYTES = 5_000_000 + +export interface DiffImageSource { + bytes: number + read: () => Promise +} + +export function imageMime(file: string): string | undefined { + return MIMES[path.extname(file).toLowerCase()] +} + +export function encodeImageSide(mime: string, data: Buffer | undefined, bytes = data?.byteLength ?? 0): DiffImageSide { + if (bytes > MAX_IMAGE_BYTES) return { mime, bytes, error: "too-large" } + if (!data || data.byteLength === 0) return { mime, bytes, error: "unreadable" } + if (data.byteLength > MAX_IMAGE_BYTES) return { mime, bytes: data.byteLength, error: "too-large" } + return { mime, bytes: data.byteLength, data: data.toString("base64") } +} + +export function readImageFile(file: string): Promise { + return new Promise((resolve) => { + const chunks: Buffer[] = [] + let bytes = 0 + const stream = createReadStream(file, { end: MAX_IMAGE_BYTES, highWaterMark: 64 * 1024 }) + stream.on("data", (chunk) => { + const data = typeof chunk === "string" ? Buffer.from(chunk) : chunk + chunks.push(data) + bytes += data.byteLength + }) + stream.on("error", () => resolve(undefined)) + stream.on("end", () => resolve(Buffer.concat(chunks, bytes))) + }) +} + +async function load(mime: string, source: DiffImageSource): Promise { + if (source.bytes > MAX_IMAGE_BYTES) return { mime, bytes: source.bytes, error: "too-large" } + const data = await source.read().catch(() => undefined) + return encodeImageSide(mime, data, source.bytes) +} + +export async function loadImage( + file: string, + before?: DiffImageSource, + after?: DiffImageSource, +): Promise { + const mime = imageMime(file) + if (!mime) return undefined + const [left, right] = await Promise.all([ + before ? load(mime, before) : undefined, + after ? load(mime, after) : undefined, + ]) + return { before: left, after: right } +} diff --git a/packages/kilo-vscode/src/diff/shared/path.ts b/packages/kilo-vscode/src/diff/shared/path.ts new file mode 100644 index 00000000000..3d0f9d7e0df --- /dev/null +++ b/packages/kilo-vscode/src/diff/shared/path.ts @@ -0,0 +1,9 @@ +import * as path from "path" + +export function resolveInside(dir: string, file: string): string | undefined { + if (path.isAbsolute(file)) return undefined + const full = path.resolve(dir, file) + const base = path.resolve(dir) + if (full !== base && !full.startsWith(base + path.sep)) return undefined + return full +} diff --git a/packages/kilo-vscode/src/diff/sources/git-status.ts b/packages/kilo-vscode/src/diff/sources/git-status.ts index 44cf3001ac7..b6844af9aa1 100644 --- a/packages/kilo-vscode/src/diff/sources/git-status.ts +++ b/packages/kilo-vscode/src/diff/sources/git-status.ts @@ -2,9 +2,10 @@ // off `git diff` variants and need the same name-status + numstat stitching. import * as fs from "fs/promises" -import * as path from "path" import type { GitOps } from "../../agent-manager/GitOps" import { generatedLike } from "../../agent-manager/local-diff" +import { imageMime, readImageFile } from "../shared/image" +import { resolveInside } from "../shared/path" import type { DiffFile } from "../types" export { MAX_DETAIL_BYTES } from "../../agent-manager/local-diff" @@ -51,6 +52,19 @@ export function parseNumstat(stdout: string): Map { + const map = new Map() + for (const line of stdout.split("\n")) { + const tab = line.indexOf("\t") + if (!line.startsWith(":") || tab < 0) continue + const meta = line.slice(1, tab).split(" ") + const file = line.slice(tab + 1) + if (!file || !meta[2] || !meta[3]) continue + map.set(file, { before: meta[2], after: meta[3] }) + } + return map +} + function statusFromCode(code: string): Status { if (code.startsWith("A")) return "added" if (code.startsWith("D")) return "deleted" @@ -62,6 +76,7 @@ function statusFromCode(code: string): Status { * are left empty — the controller fetches detail lazily through `fetchFile`. */ export function summarize(entry: FileEntry): DiffFile { + const image = imageMime(entry.file) !== undefined return { file: entry.file, before: "", @@ -72,13 +87,15 @@ export function summarize(entry: FileEntry): DiffFile { tracked: entry.tracked, generatedLike: generatedLike(entry.file), // Binary metadata is complete because no deferred text body exists. - summarized: !entry.binary, + // Images are the exception: their encoded sides load lazily on expansion. + summarized: image || !entry.binary, // Synthetic stamp keyed on the stats we actually polled: any change to // the file's diff produces new additions/deletions, which invalidates // the webview-side cached detail via mergeWorktreeDiffs. Callers can // supply a custom stamp (see `FileEntry.stamp`) when additions/deletions // aren't a reliable change signal — notably untracked files. stamp: entry.stamp ?? `${entry.status}:${entry.additions}:${entry.deletions}`, + kind: image ? "image" : undefined, } } @@ -95,6 +112,11 @@ export async function showBlob(git: GitOps, dir: string, ref: string, file: stri return result.code === 0 ? result.stdout : "" } +export async function showBlobBytes(git: GitOps, dir: string, ref: string, file: string): Promise { + const result = await git.execGitBuffer(["show", `${ref}:${file}`], dir) + return result.code === 0 ? result.stdout : undefined +} + /** * Read a working-tree file off disk, matching git's blob semantics for * symlinks: when the entry is a symlink, return its target string (what git @@ -116,6 +138,14 @@ export async function readDisk(dir: string, file: string): Promise { return fs.readFile(full, "utf-8").catch(() => "") } +export async function readDiskBytes(dir: string, file: string): Promise { + const full = resolveInside(dir, file) + if (!full) return undefined + const stat = await fs.lstat(full).catch(() => undefined) + if (!stat?.isFile()) return undefined + return readImageFile(full) +} + // Used to size-cap detail reads without materializing the blob. Mirrors // local-diff.ts's `blobSize` helper. export async function blobSize(git: GitOps, dir: string, ref: string, file: string): Promise { @@ -124,6 +154,19 @@ export async function blobSize(git: GitOps, dir: string, ref: string, file: stri return parseInt(result.stdout.trim(), 10) || 0 } +export async function blobOid(git: GitOps, dir: string, ref: string, file: string): Promise { + const result = await git.execGit(["rev-parse", "--verify", `${ref}:${file}`], dir) + return result.code === 0 ? result.stdout.trim() : "missing" +} + +export async function diskStamp(dir: string, file: string): Promise { + const full = resolveInside(dir, file) + if (!full) return "missing" + const stat = await fs.lstat(full).catch(() => undefined) + if (!stat) return "missing" + return `${stat.size}:${stat.mtimeMs}` +} + /** * Size of the working-tree entry at `file`. Uses `lstat` so symlinks report * the link's own size (length of the target string) instead of resolving to @@ -135,13 +178,3 @@ export async function fileSize(dir: string, file: string): Promise { const stat = await fs.lstat(full).catch(() => undefined) return stat?.size ?? 0 } - -// Rejects absolute paths and any `..` traversal that would escape `dir`. -// Returns the resolved path when safe, `undefined` otherwise. -export function resolveInside(dir: string, file: string): string | undefined { - if (path.isAbsolute(file)) return undefined - const full = path.resolve(dir, file) - const base = path.resolve(dir) - if (full !== base && !full.startsWith(base + path.sep)) return undefined - return full -} diff --git a/packages/kilo-vscode/src/diff/sources/session.ts b/packages/kilo-vscode/src/diff/sources/session.ts index c5c16242cd2..58fc6d8bdd4 100644 --- a/packages/kilo-vscode/src/diff/sources/session.ts +++ b/packages/kilo-vscode/src/diff/sources/session.ts @@ -1,5 +1,7 @@ +import { createHash } from "crypto" import type { SnapshotFileDiff } from "@kilocode/sdk/v2/client" import { normalize, text } from "@kilocode/kilo-ui/session-diff" +import { encodeImageSide, imageMime } from "../shared/image" import type { DiffFile } from "../types" import type { DiffSource, DiffSourceDescriptor, DiffSourceFetch } from "./types" @@ -36,6 +38,7 @@ export function createSessionDiffSource( ): DiffSource { // Cached across fetches so subsequent polling ticks skip the config lookup. let snapshotsDisabled = false + let cache: { key: string; diffs: DiffFile[] } | undefined return { descriptor: sessionDescriptor(sessionId), @@ -54,24 +57,50 @@ export function createSessionDiffSource( } const raw = await fetch({ sessionID: sessionId, directory: workspaceRoot }) - return { diffs: raw.map(toSessionDiffFile) } + const key = raw.map(fingerprint).join("|") + if (cache?.key === key) return { diffs: cache.diffs } + const diffs = raw.map(toSessionDiffFile) + cache = { key, diffs } + return { diffs } }, } } +function fingerprint(raw: SnapshotFileDiff): string { + const patch = createHash("sha1") + .update(raw.patch ?? "") + .digest("hex") + return [raw.file, raw.status, raw.additions, raw.deletions, patch].join(":") +} + /** * Project a backend `SnapshotFileDiff` onto the `DiffFile` shape the viewer * expects. Shared with `createTurnDiffSource` since both hit the same endpoint. */ export function toSessionDiffFile(raw: SnapshotFileDiff): DiffFile { + const file = raw.file ?? "" + const mime = imageMime(file) // Empty patch means binary or summarized (>256 KB) — normalize() can't - // parse it, so short-circuit to empty strings. - const view = raw.patch === "" ? null : normalize(raw) + // parse it, so short-circuit to empty strings. Binary snapshot images do + // not retain their sides, while text-backed SVG patches can be rebuilt. + const view = raw.patch === "" || (mime && mime !== "image/svg+xml") ? null : normalize(raw) + const before = view ? text(view, "deletions") : "" + const after = view ? text(view, "additions") : "" + const image = (() => { + if (mime === "image/svg+xml" && view) { + return { + before: raw.status === "added" ? undefined : encodeImageSide(mime, Buffer.from(before)), + after: raw.status === "deleted" ? undefined : encodeImageSide(mime, Buffer.from(after)), + } + } + if (mime) return {} + return undefined + })() return { - file: raw.file ?? "", - before: view ? text(view, "deletions") : "", - after: view ? text(view, "additions") : "", - patch: raw.patch, + file, + before: mime ? "" : before, + after: mime ? "" : after, + patch: mime ? "" : raw.patch, additions: raw.additions, deletions: raw.deletions, status: raw.status, @@ -79,6 +108,9 @@ export function toSessionDiffFile(raw: SnapshotFileDiff): DiffFile { generatedLike: false, // A zero-stat empty patch has no text body to fetch; nonzero stats // indicate a deferred large-file summary. - summarized: raw.patch === "" && (raw.additions !== 0 || raw.deletions !== 0), + summarized: !mime && raw.patch === "" && (raw.additions !== 0 || raw.deletions !== 0), + kind: mime ? "image" : undefined, + image, + stamp: mime ? fingerprint(raw) : undefined, } } diff --git a/packages/kilo-vscode/src/diff/sources/staged.ts b/packages/kilo-vscode/src/diff/sources/staged.ts index c47cc552fb3..d21a508902b 100644 --- a/packages/kilo-vscode/src/diff/sources/staged.ts +++ b/packages/kilo-vscode/src/diff/sources/staged.ts @@ -2,15 +2,20 @@ import * as vscode from "vscode" import { GitOps } from "../../agent-manager/GitOps" import { generatedLike } from "../../agent-manager/local-diff" import { appendOutput, getWorkspaceRoot } from "../../review-utils" +import { imageMime, loadImage } from "../shared/image" +import { resolveInside } from "../shared/path" import type { DiffFile } from "../types" import type { DiffSource, DiffSourceDescriptor, DiffSourceFetch } from "./types" import { + blobOid, blobSize, INDEX_REF, MAX_DETAIL_BYTES, parseNameStatus, parseNumstat, + parseRawOids, showBlob, + showBlobBytes, summarize, type FileEntry, } from "./git-status" @@ -24,6 +29,11 @@ export const STAGED_DESCRIPTOR: DiffSourceDescriptor = { capabilities: { revert: false, comments: true }, } +function stamp(entry: FileEntry, before: string, after: string): FileEntry { + if (!imageMime(entry.file)) return entry + return { ...entry, stamp: `${entry.status}:${before}:${after}` } +} + /** * Diff between the git index and HEAD — what `git diff --cached` would show. * Polls on the standard interval; revert isn't supported (use `git reset` from @@ -37,24 +47,36 @@ export function createStagedDiffSource(): DiffSource { const root = (): string | undefined => getWorkspaceRoot() const listEntries = async (dir: string): Promise => { - const [nameStatus, numstat] = await Promise.all([ + const [nameStatus, numstat, raw] = await Promise.all([ git.execGit(["-c", "core.quotepath=false", "diff", "--cached", "--name-status", "--no-renames", "HEAD"], dir), git.execGit(["-c", "core.quotepath=false", "diff", "--cached", "--numstat", "--no-renames", "HEAD"], dir), + git.execGit( + ["-c", "core.quotepath=false", "diff", "--cached", "--raw", "--abbrev=64", "--no-renames", "HEAD"], + dir, + ), ]) if (nameStatus.code !== 0) { log("git diff --cached --name-status failed", { code: nameStatus.code, stderr: nameStatus.stderr.trim() }) return [] } const counts = parseNumstat(numstat.code === 0 ? numstat.stdout : "") - const items = parseNameStatus(nameStatus.stdout) - return items.map((item) => ({ - file: item.file, - status: item.status, - additions: counts.get(item.file)?.additions ?? 0, - deletions: counts.get(item.file)?.deletions ?? 0, - tracked: true, - binary: counts.get(item.file)?.binary ?? false, - })) + const refs = parseRawOids(raw.code === 0 ? raw.stdout : "") + return parseNameStatus(nameStatus.stdout).map((item) => { + const ref = refs.get(item.file) + const entry = { + file: item.file, + status: item.status, + additions: counts.get(item.file)?.additions ?? 0, + deletions: counts.get(item.file)?.deletions ?? 0, + tracked: true, + binary: counts.get(item.file)?.binary ?? false, + } + return stamp( + entry, + item.status === "added" ? "missing" : (ref?.before ?? "missing"), + item.status === "deleted" ? "missing" : (ref?.after ?? "missing"), + ) + }) } return { @@ -73,7 +95,7 @@ export function createStagedDiffSource(): DiffSource { async fetchFile(file: string): Promise { const dir = root() - if (!dir || !file) return null + if (!dir || !file || !resolveInside(dir, file)) return null // Resolve the entry for this single file so we know its status. Reading // both refs blindly would still work, but knowing the status lets us @@ -81,10 +103,23 @@ export function createStagedDiffSource(): DiffSource { const entry = await fileEntry(git, dir, file, log) if (!entry) return null - if (entry.binary) return summarize(entry) - + const mime = imageMime(file) + if (entry.binary && !mime) return summarize(entry) const beforeBytes = entry.status === "added" ? 0 : await blobSize(git, dir, "HEAD", file) const afterBytes = entry.status === "deleted" ? 0 : await blobSize(git, dir, INDEX_REF, file) + if (mime) { + const image = await loadImage( + file, + entry.status === "added" + ? undefined + : { bytes: beforeBytes, read: () => showBlobBytes(git, dir, "HEAD", file) }, + entry.status === "deleted" + ? undefined + : { bytes: afterBytes, read: () => showBlobBytes(git, dir, INDEX_REF, file) }, + ) + return { ...summarize(entry), summarized: false, image } + } + if (beforeBytes > MAX_DETAIL_BYTES || afterBytes > MAX_DETAIL_BYTES) { log("Staged detail skipped: file too large", { file, beforeBytes, afterBytes, cap: MAX_DETAIL_BYTES }) return summarize(entry) @@ -110,7 +145,7 @@ export function createStagedDiffSource(): DiffSource { tracked: true, generatedLike: generatedLike(file), summarized, - stamp: `${entry.status}:${entry.additions}:${entry.deletions}`, + stamp: entry.stamp ?? `${entry.status}:${entry.additions}:${entry.deletions}`, } }, @@ -144,7 +179,7 @@ async function fileEntry( dir, ) const stats = parseNumstat(counts.code === 0 ? counts.stdout : "") - return { + const entry = { file: item.file, status: item.status, additions: stats.get(item.file)?.additions ?? 0, @@ -152,4 +187,10 @@ async function fileEntry( tracked: true, binary: stats.get(item.file)?.binary ?? false, } + if (!imageMime(item.file)) return entry + const [before, after] = await Promise.all([ + item.status === "added" ? "missing" : blobOid(git, dir, "HEAD", item.file), + item.status === "deleted" ? "missing" : blobOid(git, dir, INDEX_REF, item.file), + ]) + return stamp(entry, before, after) } diff --git a/packages/kilo-vscode/src/diff/sources/unstaged.ts b/packages/kilo-vscode/src/diff/sources/unstaged.ts index 3aeb5db1704..b4d0c996161 100644 --- a/packages/kilo-vscode/src/diff/sources/unstaged.ts +++ b/packages/kilo-vscode/src/diff/sources/unstaged.ts @@ -4,18 +4,24 @@ import { GitOps } from "../../agent-manager/GitOps" import { generatedLike } from "../../agent-manager/local-diff" import { appendOutput, getWorkspaceRoot } from "../../review-utils" import { binaryFile } from "../shared/binary" +import { imageMime, loadImage } from "../shared/image" +import { resolveInside } from "../shared/path" import type { DiffFile } from "../types" import type { DiffSource, DiffSourceDescriptor, DiffSourceFetch } from "./types" import { + blobOid, blobSize, + diskStamp, fileSize, INDEX_REF, MAX_DETAIL_BYTES, parseNameStatus, parseNumstat, + parseRawOids, readDisk, - resolveInside, + readDiskBytes, showBlob, + showBlobBytes, summarize, type FileEntry, } from "./git-status" @@ -29,6 +35,11 @@ export const UNSTAGED_DESCRIPTOR: DiffSourceDescriptor = { capabilities: { revert: false, comments: true }, } +function stamp(entry: FileEntry, before: string, after: string): FileEntry { + if (!imageMime(entry.file)) return entry + return { ...entry, stamp: `${entry.status}:${before}:${after}` } +} + /** * Diff between the working tree and the index — what `git diff` shows for * tracked files, plus untracked files (treated as fully-added). Read-only; @@ -42,23 +53,33 @@ export function createUnstagedDiffSource(): DiffSource { const root = (): string | undefined => getWorkspaceRoot() const listTracked = async (dir: string): Promise => { - const [nameStatus, numstat] = await Promise.all([ + const [nameStatus, numstat, raw] = await Promise.all([ git.execGit(["-c", "core.quotepath=false", "diff", "--name-status", "--no-renames"], dir), git.execGit(["-c", "core.quotepath=false", "diff", "--numstat", "--no-renames"], dir), + git.execGit(["-c", "core.quotepath=false", "diff", "--raw", "--abbrev=64", "--no-renames"], dir), ]) if (nameStatus.code !== 0) { log("git diff --name-status failed", { code: nameStatus.code, stderr: nameStatus.stderr.trim() }) return [] } const counts = parseNumstat(numstat.code === 0 ? numstat.stdout : "") - return parseNameStatus(nameStatus.stdout).map((item) => ({ - file: item.file, - status: item.status, - additions: counts.get(item.file)?.additions ?? 0, - deletions: counts.get(item.file)?.deletions ?? 0, - tracked: true, - binary: counts.get(item.file)?.binary ?? false, - })) + const refs = parseRawOids(raw.code === 0 ? raw.stdout : "") + return Promise.all( + parseNameStatus(nameStatus.stdout).map(async (item) => { + const entry = { + file: item.file, + status: item.status, + additions: counts.get(item.file)?.additions ?? 0, + deletions: counts.get(item.file)?.deletions ?? 0, + tracked: true, + binary: counts.get(item.file)?.binary ?? false, + } + if (!imageMime(item.file)) return entry + const before = item.status === "added" ? "missing" : (refs.get(item.file)?.before ?? "missing") + const after = item.status === "deleted" ? "missing" : await diskStamp(dir, item.file) + return stamp(entry, before, after) + }), + ) } const listUntracked = async (dir: string): Promise => { @@ -116,17 +137,24 @@ export function createUnstagedDiffSource(): DiffSource { async fetchFile(file: string): Promise { const dir = root() - if (!dir || !file) return null + if (!dir || !file || !resolveInside(dir, file)) return null const entry = await fileEntry(git, dir, file, log) if (!entry) return null + const mime = imageMime(file) + if (entry.binary && !mime) return summarize(entry) - if (entry.binary) return summarize(entry) + const bytes = await detailBytes(git, dir, file, entry) + const image = await imageDetail(git, dir, file, entry, bytes.before, bytes.after) + if (image) return image - const beforeBytes = !entry.tracked || entry.status === "added" ? 0 : await blobSize(git, dir, INDEX_REF, file) - const afterBytes = entry.status === "deleted" ? 0 : await fileSize(dir, file) - if (beforeBytes > MAX_DETAIL_BYTES || afterBytes > MAX_DETAIL_BYTES) { - log("Unstaged detail skipped: file too large", { file, beforeBytes, afterBytes, cap: MAX_DETAIL_BYTES }) + if (bytes.before > MAX_DETAIL_BYTES || bytes.after > MAX_DETAIL_BYTES) { + log("Unstaged detail skipped: file too large", { + file, + beforeBytes: bytes.before, + afterBytes: bytes.after, + cap: MAX_DETAIL_BYTES, + }) return summarize(entry) } @@ -170,6 +198,31 @@ export function createUnstagedDiffSource(): DiffSource { } } +async function detailBytes(git: GitOps, dir: string, file: string, entry: FileEntry) { + const before = !entry.tracked || entry.status === "added" ? 0 : await blobSize(git, dir, INDEX_REF, file) + const after = entry.status === "deleted" ? 0 : await fileSize(dir, file) + return { before, after } +} + +async function imageDetail( + git: GitOps, + dir: string, + file: string, + entry: FileEntry, + beforeBytes: number, + afterBytes: number, +): Promise { + if (!imageMime(file)) return undefined + const image = await loadImage( + file, + !entry.tracked || entry.status === "added" + ? undefined + : { bytes: beforeBytes, read: () => showBlobBytes(git, dir, INDEX_REF, file) }, + entry.status === "deleted" ? undefined : { bytes: afterBytes, read: () => readDiskBytes(dir, file) }, + ) + return { ...summarize(entry), summarized: false, image } +} + async function fileEntry( git: GitOps, dir: string, @@ -190,7 +243,7 @@ async function fileEntry( dir, ) const stats = parseNumstat(counts.code === 0 ? counts.stdout : "") - return { + const entry = { file: item.file, status: item.status, additions: stats.get(item.file)?.additions ?? 0, @@ -198,6 +251,12 @@ async function fileEntry( tracked: true, binary: stats.get(item.file)?.binary ?? false, } + if (!imageMime(item.file)) return entry + const [before, after] = await Promise.all([ + item.status === "added" ? "missing" : blobOid(git, dir, INDEX_REF, item.file), + item.status === "deleted" ? "missing" : diskStamp(dir, item.file), + ]) + return stamp(entry, before, after) } } @@ -210,6 +269,8 @@ async function fileEntry( log("Unstaged file rejected: outside workspace", { file }) return undefined } + const untracked = await git.execGit(["ls-files", "--others", "--exclude-standard", "--", file], dir) + if (untracked.code !== 0 || !untracked.stdout.split("\n").includes(file)) return undefined const stat = await fs.lstat(full).catch(() => undefined) if (!stat) { log("Unstaged file not found", { file }) diff --git a/packages/kilo-vscode/src/diff/sources/worktree.ts b/packages/kilo-vscode/src/diff/sources/worktree.ts index 36a436fce3a..1f3d8ee25a4 100644 --- a/packages/kilo-vscode/src/diff/sources/worktree.ts +++ b/packages/kilo-vscode/src/diff/sources/worktree.ts @@ -156,5 +156,7 @@ function toDiffFile(entry: WorktreeDiffEntry): DiffFile { generatedLike: entry.generatedLike, summarized: entry.summarized, stamp: entry.stamp, + kind: entry.kind, + image: entry.image, } } diff --git a/packages/kilo-vscode/src/diff/types.ts b/packages/kilo-vscode/src/diff/types.ts index a6d9def400f..913d4560a31 100644 --- a/packages/kilo-vscode/src/diff/types.ts +++ b/packages/kilo-vscode/src/diff/types.ts @@ -12,6 +12,20 @@ export interface PanelContext { baseBranchOverride?: string } +export type DiffImageError = "too-large" | "unreadable" + +export interface DiffImageSide { + mime: string + bytes: number + data?: string + error?: DiffImageError +} + +export interface DiffImage { + before?: DiffImageSide + after?: DiffImageSide +} + /** Mirrors `WorktreeFileDiff` in webview-ui/src/types/messages/agent-manager.ts. */ export interface DiffFile { file: string @@ -26,4 +40,6 @@ export interface DiffFile { generatedLike?: boolean summarized?: boolean stamp?: string + kind?: "image" + image?: DiffImage } diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index 35dc3073d2b..c681c44aee3 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -435,6 +435,9 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("kilo-code.new.agentManager.nextTab", () => { agentManagerProvider.postMessage({ type: "action", action: "tabNext" }) }), + vscode.commands.registerCommand("kilo-code.new.agentManager.search", () => { + agentManagerProvider.postMessage({ type: "action", action: "search" }) + }), vscode.commands.registerCommand("kilo-code.new.agentManager.showTerminal", () => { // Route through the webview so it can reach into the active session // state and open the VS Code integrated terminal for it. @@ -491,12 +494,14 @@ export function activate(context: vscode.ExtensionContext) { return } - if (uri.path !== "/kilocode/model") return - const modelID = new URLSearchParams(uri.query).get("model") - if (!modelID) return - console.log("[Kilo New] URI handler: selecting linked Kilo model:", modelID) + if (uri.path !== "/kilocode/switch" && uri.path !== "/kilocode/model") return + const params = new URLSearchParams(uri.query) + const modelID = params.get("model") || undefined + const agent = params.get("agent") || undefined + if (!modelID && !agent) return + console.log("[Kilo New] URI handler: applying linked Kilo selection:", { modelID, agent }) await vscode.commands.executeCommand(`${KiloProvider.viewType}.focus`) - provider.selectKiloModel(modelID) + provider.selectKiloModel(modelID, agent) }, }), ) diff --git a/packages/kilo-vscode/src/provider-actions.ts b/packages/kilo-vscode/src/provider-actions.ts index 739ff339e9d..742380b5604 100644 --- a/packages/kilo-vscode/src/provider-actions.ts +++ b/packages/kilo-vscode/src/provider-actions.ts @@ -9,7 +9,7 @@ import { sanitizeCustomProviderConfig, withCustomProviderDeletions, } from "./shared/custom-provider" -import { isCustomProviderPackage, KILO_AUTO, parseModelString } from "./shared/provider-model" +import { isCustomProviderPackage, KILO_AUTO, KILO_PROVIDER_ID, parseModelString } from "./shared/provider-model" import { configFeatures } from "./features" /** @@ -50,7 +50,7 @@ function same(a: unknown, b: unknown): boolean { return akeys.every((key, index) => key === bkeys[index] && same(a[key], b[key])) } -/** Fetch auth methods alongside the provider list. Auth states default to empty (endpoint not yet available). */ +/** Fetch provider availability and authentication state without exposing stored credentials. */ export async function fetchProviderData(client: KiloClient, dir: string) { const authRequest = typeof client.provider.auth === "function" @@ -59,10 +59,15 @@ export async function fetchProviderData(client: KiloClient, dir: string) { .then((r) => r.data ?? {}) .catch(() => ({})) : Promise.resolve({}) + const kiloRequest = client.kilo + .authStatus({ directory: dir }, { throwOnError: true }) + .then((r) => (r.data?.authenticated ? (r.data.type ?? null) : null)) + .catch(() => null) - const [{ data: response }, authMethods] = await Promise.all([ + const [{ data: response }, authMethods, kiloAuth] = await Promise.all([ client.provider.list({ directory: dir }, { throwOnError: true }), authRequest, + kiloRequest, ]) const authStates: Record = {} const storedKeys: Record = {} @@ -83,6 +88,8 @@ export async function fetchProviderData(client: KiloClient, dir: string) { delete next.key return next as (typeof response.all)[number] }) + delete authStates[KILO_PROVIDER_ID] + if (kiloAuth) authStates[KILO_PROVIDER_ID] = kiloAuth return { response: { ...response, all }, authMethods, authStates, storedKeys } } diff --git a/packages/kilo-vscode/src/review-utils.ts b/packages/kilo-vscode/src/review-utils.ts index 42dac76b284..c0d7eccbeda 100644 --- a/packages/kilo-vscode/src/review-utils.ts +++ b/packages/kilo-vscode/src/review-utils.ts @@ -23,15 +23,17 @@ export function openFileInEditor( prefix = "Kilo", ): void { const uri = vscode.Uri.file(filePath) - const target = Math.max(1, Math.floor(line ?? 1)) - const col = column !== undefined && column > 0 ? column - 1 : 0 - const pos = new vscode.Position(target - 1, col) - const selection = new vscode.Range(pos, pos) + const options: vscode.TextDocumentShowOptions = { viewColumn, preview: true } + if (line !== undefined && line > 0) { + const target = Math.max(1, Math.floor(line)) + const col = column !== undefined && column > 0 ? column - 1 : 0 + const pos = new vscode.Position(target - 1, col) + options.selection = new vscode.Range(pos, pos) + } - vscode.workspace.openTextDocument(uri).then( - (doc) => vscode.window.showTextDocument(doc, { viewColumn, preview: true, selection }), - (err) => console.error(`[Kilo New] ${prefix}: Failed to open file:`, uri.fsPath, err), - ) + void vscode.commands + .executeCommand("vscode.open", uri, options) + .then(undefined, (err) => console.error(`[Kilo New] ${prefix}: Failed to open file:`, uri.fsPath, err)) } export function openWorkspaceRelativeFile(relativePath: string, line?: number, column?: number): void { diff --git a/packages/kilo-vscode/src/speech-to-text/models.ts b/packages/kilo-vscode/src/speech-to-text/models.ts index be872d81c96..7eab1dfa181 100644 --- a/packages/kilo-vscode/src/speech-to-text/models.ts +++ b/packages/kilo-vscode/src/speech-to-text/models.ts @@ -6,6 +6,11 @@ export interface SpeechToTextModelDef { } const models: SpeechToTextModelDef[] = [ + { + id: "openai/whisper-large-v3-turbo", + label: "Whisper Large V3 Turbo", + provider: "OpenAI-compatible", + }, { id: "openai/gpt-4o-mini-transcribe", label: "GPT-4o Mini Transcribe", @@ -23,11 +28,6 @@ const models: SpeechToTextModelDef[] = [ label: "Whisper 1", provider: "OpenAI", }, - { - id: "openai/whisper-large-v3-turbo", - label: "Whisper Large V3 Turbo", - provider: "OpenAI-compatible", - }, { id: "openai/whisper-large-v3", label: "Whisper Large V3", diff --git a/packages/kilo-vscode/tests/accessibility.spec.ts b/packages/kilo-vscode/tests/accessibility.spec.ts index 4600a9f0cd6..2f1565ab4df 100644 --- a/packages/kilo-vscode/tests/accessibility.spec.ts +++ b/packages/kilo-vscode/tests/accessibility.spec.ts @@ -13,6 +13,7 @@ const STORIES = [ { id: "settings--providers-configure", name: "Settings / providers empty state" }, { id: "marketplace--skills-tab-empty", name: "Marketplace / skills empty state" }, { id: "marketplace--agents-tab-empty", name: "Marketplace / agents empty state" }, + { id: "agentmanager--sidebar-search-open", name: "Agent Manager / sidebar search" }, ] function url(id: string) { @@ -61,4 +62,61 @@ test.describe("webview accessibility ratchet", () => { await page.keyboard.press("Enter") await expect(login).toHaveAttribute("data-keyboard-activated", "true") }) + + test("Agent Manager sidebar search filters and selects with the keyboard", async ({ page }) => { + await open(page, "agentmanager--sidebar-search-open") + + const trigger = page.getByRole("button", { name: "Search worktrees and sessions" }) + const input = page.getByPlaceholder("Search worktrees and sessions", { exact: true }) + const prompt = page.getByRole("textbox", { name: "Story prompt" }) + await expect(input).toBeFocused() + await expect(page.locator('[data-slot="list-header"]')).toHaveText(["SESSIONS", "LOCAL & WORKTREES"]) + + await input.fill("Render") + await expect(page.locator('[data-slot="sidebar-search-result"]').first()).toContainText( + "Render images in diff viewer", + ) + await input.fill("rndr img") + await expect(page.locator('[data-slot="sidebar-search-result"]').first()).toContainText( + "Render images in diff viewer", + ) + + await input.fill("main") + await expect(page.locator('[data-slot="sidebar-search-result"]').first()).toContainText("local") + await page.keyboard.press("Enter") + await expect(page.locator('[data-slot="sidebar-search-selection"]')).toHaveText("local") + await expect(prompt).toBeFocused() + + await trigger.click() + await input.fill("Build grouped") + await expect(page.getByText("Build grouped worktree search", { exact: true })).toBeVisible() + + await page.keyboard.press("Enter") + await expect(page.locator('[data-slot="sidebar-search-selection"]')).toHaveText("session:session-build") + await expect(input).toBeHidden() + await expect(prompt).toBeFocused() + + await trigger.click() + await input.fill("local indexing") + await expect(page.getByText("Investigate local indexing", { exact: true })).toBeVisible() + await page.keyboard.press("Enter") + await expect(page.locator('[data-slot="sidebar-search-selection"]')).toHaveText("session:session-local") + + await trigger.click() + await input.fill("does not exist") + await expect(page.locator('[data-slot="list-empty-state"]')).toBeVisible() + await page.keyboard.press("Escape") + await expect(prompt).toBeFocused() + + await trigger.click() + await expect(input).toBeFocused() + await page.locator(".am-section-label").click() + await expect(prompt).toBeFocused() + + await trigger.hover() + await expect( + page.getByText("Searches the local workspace, local sessions, worktrees, and their sessions", { exact: true }), + ).toBeVisible() + await expect(page.getByText("⌘F", { exact: true })).toBeVisible() + }) }) diff --git a/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts b/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts index dd634e67ff9..fcd2111199e 100644 --- a/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts +++ b/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts @@ -133,6 +133,13 @@ test("settings and mode editing expose distinct model field purposes", async ({ /Override the default model for specific modes/, ) + await load(page, "settings--models-speech-to-text") + const speech = page.getByRole("button", { name: "Speech to Text Model: Chirp 3" }) + await expect(speech).toBeEnabled() + await speech.click() + await page.getByRole("option", { name: "GPT-4o Mini Transcribe (OpenAI)" }).click() + await expect(page.getByRole("button", { name: "Speech to Text Model: GPT-4o Mini Transcribe" })).toBeVisible() + await load(page, "settings--mode-edit-export") await expect(page.getByRole("button", { name: /Model Override:/ })).toHaveAccessibleDescription( "Override the default model for this agent", diff --git a/packages/kilo-vscode/tests/permission-diff.spec.ts b/packages/kilo-vscode/tests/permission-diff.spec.ts new file mode 100644 index 00000000000..15697a0b95d --- /dev/null +++ b/packages/kilo-vscode/tests/permission-diff.spec.ts @@ -0,0 +1,12 @@ +import { expect, test } from "@playwright/test" + +const STORY_ID = "composite-webview--permission-dock-edit" +const GLOBALS = "colorScheme:dark;theme:kilo-vscode;vscodeTheme:dark-modern" + +test("edit approval diff shows line numbers in compact viewer", async ({ page }) => { + await page.setViewportSize({ width: 420, height: 720 }) + await page.goto(`/iframe.html?id=${STORY_ID}&viewMode=story&globals=${GLOBALS}`, { waitUntil: "load" }) + + const number = page.locator('[data-slot="permission-diff-content"] [data-column-number]').first() + await expect(number).toBeVisible() +}) diff --git a/packages/kilo-vscode/tests/settings-accessibility.spec.ts b/packages/kilo-vscode/tests/settings-accessibility.spec.ts new file mode 100644 index 00000000000..e9c93e86e38 --- /dev/null +++ b/packages/kilo-vscode/tests/settings-accessibility.spec.ts @@ -0,0 +1,55 @@ +import { expect, test, type Page } from "@playwright/test" + +const GLOBALS = "colorScheme:dark;theme:kilo-vscode;vscodeTheme:dark-modern" +const NAMES = [ + "Models", + "Providers", + "Agent Behaviour", + "Auto-Approve", + "Browser", + "Checkpoints", + "Display", + "Autocomplete", + "Notifications", + "Context", + "Commit Message", + "Experimental", + "Language", + "About Kilo Code", +] + +function story(page: Page) { + return page.goto(`/iframe.html?id=settings--settings-panel&viewMode=story&globals=${GLOBALS}`, { + waitUntil: "load", + }) +} + +test.describe("settings tab accessibility", () => { + test("exposes named tabs and selected state in the compact sidebar", async ({ page }) => { + await page.setViewportSize({ width: 420, height: 720 }) + await story(page) + + const tabs = page.getByRole("tab") + await expect(tabs).toHaveCount(NAMES.length) + for (const name of NAMES) { + await expect(page.getByRole("tab", { name, exact: true })).toBeVisible() + } + + const models = page.getByRole("tab", { name: "Models" }) + const providers = page.getByRole("tab", { name: "Providers" }) + await expect(models).toHaveAttribute("aria-selected", "true") + await expect(providers).toHaveAttribute("aria-selected", "false") + await expect(page.getByRole("tabpanel", { name: "Models" })).toBeVisible() + + await models.focus() + await page.keyboard.press("ArrowDown") + await expect(providers).toBeFocused() + await expect(providers).toHaveAttribute("aria-selected", "true") + await expect(page.getByRole("tabpanel", { name: "Providers" })).toBeVisible() + + await page.keyboard.press("ArrowUp") + await expect(models).toBeFocused() + await expect(models).toHaveAttribute("aria-selected", "true") + await expect(page.getByRole("tabpanel", { name: "Models" })).toBeVisible() + }) +}) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index d45c3873e2e..06a8e2e0ae9 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -24,6 +24,7 @@ const TSX_FILES = [ path.join(ROOT, "webview-ui/agent-manager/sortable-tab.tsx"), path.join(ROOT, "webview-ui/agent-manager/DiffPanel.tsx"), path.join(ROOT, "webview-ui/diff-viewer/FullScreenDiffView.tsx"), + path.join(ROOT, "webview-ui/diff-viewer/ImageDiffView.tsx"), path.join(ROOT, "webview-ui/diff-viewer/MarkdownDiffView.tsx"), path.join(ROOT, "webview-ui/diff-viewer/MarkdownAnnotationLayer.tsx"), path.join(ROOT, "webview-ui/diff-viewer/markdown-comment-ranges.ts"), @@ -35,8 +36,9 @@ const TSX_FILES = [ path.join(ROOT, "webview-ui/agent-manager/ApplyDialog.tsx"), path.join(ROOT, "webview-ui/agent-manager/WorktreeItem.tsx"), path.join(ROOT, "webview-ui/agent-manager/SectionHeader.tsx"), - path.join(ROOT, "webview-ui/agent-manager/CurrentTabsMenu.tsx"), + path.join(ROOT, "webview-ui/agent-manager/SidebarSearchMenu.tsx"), path.join(ROOT, "webview-ui/agent-manager/SidebarToggleButton.tsx"), + path.join(ROOT, "webview-ui/agent-manager/WorktreeSectionActions.tsx"), path.join(ROOT, "webview-ui/agent-manager/tab-rendering.tsx"), path.join(ROOT, "webview-ui/agent-manager/terminal/TerminalTab.tsx"), path.join(ROOT, "webview-ui/agent-manager/terminal/SortableTerminalTab.tsx"), diff --git a/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts index 009b459df7e..381ac518a38 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts @@ -41,6 +41,20 @@ describe("agent manager diff state", () => { expect(result.stale.size).toBe(0) }) + it("preserves loaded image data when summary metadata is unchanged", () => { + const image = { + before: { mime: "image/png", bytes: 3, data: "b2xk" }, + after: { mime: "image/png", bytes: 3, data: "bmV3" }, + } + const prev = [diff({ file: "asset.png", kind: "image", summarized: false, image })] + const next = [diff({ file: "asset.png", kind: "image", summarized: true })] + const result = mergeWorktreeDiffs(prev, next) + + expect(result.diffs[0]).toBe(prev[0]) + expect(result.diffs[0]?.image).toBe(image) + expect(result.stale.size).toBe(0) + }) + it("replaces detailed content when patch anchors change", () => { const prev = [diff({ summarized: false, before: "old\n", after: "new\n", patch: "@@ -1 +1 @@\n-old\n+new\n" })] const next = [diff({ summarized: false, before: "old\n", after: "new\n", patch: "@@ -100 +100 @@\n-old\n+new\n" })] @@ -74,6 +88,7 @@ describe("agent manager diff state", () => { diff({ file: "src/app.ts", generatedLike: false, additions: 3 }), diff({ file: "node_modules/pkg/index.js", generatedLike: true, additions: 3 }), diff({ file: "audio/notification.wav", summarized: false, additions: 0 }), + diff({ file: "assets/banner.png", kind: "image", summarized: true, additions: 0 }), diff({ file: "src/huge.ts", additions: EXTREME_DIFF_CHANGED_LINES + 1 }), ]), ).toEqual(["src/app.ts", "node_modules/pkg/index.js", "src/huge.ts"]) @@ -121,13 +136,18 @@ describe("agent manager diff state", () => { expect(toggleOpenFiles(diffs, files)).toEqual([]) }) - it("prevents non-text diffs from entering open state", () => { + it("opens images while preventing other non-text diffs from entering open state", () => { const audio = diff({ file: "audio/alert.wav", summarized: false, additions: 0 }) + const image = diff({ file: "assets/banner.png", kind: "image", summarized: true, additions: 0 }) const text = diff({ file: "src/app.ts" }) expect(isDiffExpandable(audio)).toBe(false) + expect(isDiffExpandable(image)).toBe(true) expect(isDiffExpandable(text)).toBe(true) - expect(sanitizeOpenFiles([audio, text], [audio.file, text.file])).toEqual([text.file]) + expect(sanitizeOpenFiles([audio, image, text], [audio.file, image.file, text.file])).toEqual([ + image.file, + text.file, + ]) }) }) diff --git a/packages/kilo-vscode/tests/unit/continue-in-worktree.test.ts b/packages/kilo-vscode/tests/unit/continue-in-worktree.test.ts index 4b72acdcac0..8cf641d90a2 100644 --- a/packages/kilo-vscode/tests/unit/continue-in-worktree.test.ts +++ b/packages/kilo-vscode/tests/unit/continue-in-worktree.test.ts @@ -1,18 +1,49 @@ -import { describe, expect, it, mock } from "bun:test" +import { afterEach, describe, expect, it, mock } from "bun:test" +import * as fs from "node:fs/promises" +import * as os from "node:os" +import * as path from "node:path" +import simpleGit from "simple-git" import { abortSession, - captureState, + continueInWorktree, forkSession, registerSession, type ContinueContext, - type StepResult, } from "../../src/agent-manager/continue-in-worktree" -import type { CreateWorktreeResult } from "../../src/agent-manager/WorktreeManager" +import { WorktreeManager, type CreateWorktreeResult } from "../../src/agent-manager/WorktreeManager" import { forkText } from "../../src/agent-manager/fork-handoff" import type { Session } from "@kilocode/sdk/v2/client" const noop = () => {} const log = noop as (...args: unknown[]) => void +const dirs: string[] = [] + +afterEach(async () => { + await Promise.all(dirs.splice(0, dirs.length).map((dir) => fs.rm(dir, { recursive: true, force: true }))) +}) + +async function repo(): Promise { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "continue-worktree-")) + dirs.push(dir) + const git = simpleGit(dir) + await git.init(["--initial-branch=main"]) + await git.addConfig("user.email", "test@test.com") + await git.addConfig("user.name", "Test") + await fs.writeFile(path.join(dir, "state.txt"), "base\n") + await git.add("state.txt") + await git.commit("initial") + return dir +} + +function client(fork = mock(async () => ({ data: session("forked") }))) { + return { + session: { + abort: mock(async () => ({})), + fork, + promptAsync: mock(async () => ({})), + }, + } as never +} function session(id: string): Session { return { @@ -36,6 +67,8 @@ function ctx(overrides: Partial = {}): ContinueContext { }, createWorktreeOnDisk: async () => null, runSetupScript: async () => {}, + cleanupWorktree: async () => {}, + notifyError: noop, getStateManager: () => undefined, registerWorktreeSession: noop, registerSession: noop, @@ -154,3 +187,47 @@ describe("continue-in-worktree steps", () => { }) }) }) + +describe("continueInWorktree", () => { + it("rolls back the created worktree when Git transfer fails", async () => { + const root = await repo() + const git = simpleGit(root) + await fs.writeFile(path.join(root, "state.txt"), "local dirty\n") + + const manager = new WorktreeManager(root, noop) + const setup = mock(async () => {}) + const cleanup = mock(async () => { + if (created) await manager.removeWorktree(created.path, created.branch) + }) + const notify = mock((_error: string, _result: CreateWorktreeResult, _worktreeId: string) => {}) + const progress: Array<{ status: string; error?: string }> = [] + let created: CreateWorktreeResult | undefined + const c = ctx({ + root, + getClient: () => client(), + createWorktreeOnDisk: async (opts) => { + const value = await manager.createWorktree(opts) + created = value + const target = simpleGit(value.path) + await fs.writeFile(path.join(value.path, "state.txt"), "conflict\n") + await target.add("state.txt") + await target.commit("conflict") + return { worktree: { id: "wt-1" }, result: value } + }, + runSetupScript: setup, + cleanupWorktree: cleanup, + notifyError: notify, + }) + + await continueInWorktree(c, "source", (status, _detail, error) => progress.push({ status, error })) + + expect(progress.at(-1)?.status).toBe("error") + expect(progress.at(-1)?.error).toContain("Unstaged patch failed") + expect(setup).toHaveBeenCalledTimes(1) + expect(cleanup).toHaveBeenCalledTimes(1) + expect(notify).toHaveBeenCalledWith(expect.stringContaining("Unstaged patch failed"), created!, "wt-1") + expect(created).toBeDefined() + await expect(fs.stat(created!.path)).rejects.toThrow() + expect((await git.branchLocal()).all).not.toContain(created!.branch) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/diff-image.test.ts b/packages/kilo-vscode/tests/unit/diff-image.test.ts new file mode 100644 index 00000000000..6bb7e4df969 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/diff-image.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "bun:test" +import * as fs from "fs/promises" +import * as os from "os" +import * as path from "path" +import { imageMime, loadImage, MAX_IMAGE_BYTES, readImageFile } from "../../src/diff/shared/image" +import { parseRawOids } from "../../src/diff/sources/git-status" + +describe("diff images", () => { + it("recognizes every image format handled by the VS Code image preview", () => { + const files = [ + "photo.jpg", + "photo.jpe", + "photo.jpeg", + "image.png", + "image.bmp", + "animation.gif", + "favicon.ico", + "image.webp", + "image.avif", + "vector.svg", + ] + + for (const file of files) expect(imageMime(file)).toStartWith("image/") + expect(imageMime("ASSETS/LOGO.PNG")).toBe("image/png") + expect(imageMime("archive.zip")).toBeUndefined() + expect(imageMime("photo.tiff")).toBeUndefined() + }) + + it("parses image blob identities from a single raw diff", () => { + const before = "1".repeat(40) + const after = "2".repeat(40) + const refs = parseRawOids(`:100644 100644 ${before} ${after} M\tassets/banner.png\n`) + + expect(refs.get("assets/banner.png")).toEqual({ before, after }) + }) + + it("encodes image sides without converting bytes to text", async () => { + const before = Buffer.from([0x00, 0xff, 0x10, 0x80]) + const after = Buffer.from([0x89, 0x50, 0x4e, 0x47]) + const image = await loadImage( + "asset.png", + { bytes: before.byteLength, read: async () => before }, + { bytes: after.byteLength, read: async () => after }, + ) + + expect(image?.before).toEqual({ mime: "image/png", bytes: 4, data: before.toString("base64") }) + expect(image?.after).toEqual({ mime: "image/png", bytes: 4, data: after.toString("base64") }) + }) + + it("bounds mutable image reads to the payload cap", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "diff-image-test-")) + const file = path.join(dir, "large.png") + try { + await fs.writeFile(file, Buffer.alloc(MAX_IMAGE_BYTES + 100)) + expect((await readImageFile(file))?.byteLength).toBe(MAX_IMAGE_BYTES + 1) + } finally { + await fs.rm(dir, { recursive: true, force: true }) + } + }) + + it("does not read image sides over the webview payload cap", async () => { + let reads = 0 + const image = await loadImage("asset.webp", { + bytes: MAX_IMAGE_BYTES + 1, + read: async () => { + reads++ + return Buffer.from("unused") + }, + }) + + expect(reads).toBe(0) + expect(image?.before).toEqual({ + mime: "image/webp", + bytes: MAX_IMAGE_BYTES + 1, + error: "too-large", + }) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/diff-session-source.test.ts b/packages/kilo-vscode/tests/unit/diff-session-source.test.ts index 4db1b322db7..ce870f0abbe 100644 --- a/packages/kilo-vscode/tests/unit/diff-session-source.test.ts +++ b/packages/kilo-vscode/tests/unit/diff-session-source.test.ts @@ -91,6 +91,55 @@ describe("createSessionDiffSource.fetch", () => { expect(result.diffs[2]?.summarized).toBe(true) }) + it("rebuilds text-backed SVG snapshot sides without sending them to Pierre", async () => { + const before = '' + const after = '' + const patch = [ + "diff --git a/assets/banner.svg b/assets/banner.svg", + "--- a/assets/banner.svg", + "+++ b/assets/banner.svg", + "@@ -1 +1 @@", + `-${before}`, + `+${after}`, + "", + ].join("\n") + const { fetch } = recording([{ file: "assets/banner.svg", patch, additions: 1, deletions: 1, status: "modified" }]) + const result = await createSessionDiffSource("s-image", fetch, "/repo").fetch() + + expect(result.diffs[0]).toMatchObject({ + file: "assets/banner.svg", + before: "", + after: "", + patch: "", + kind: "image", + summarized: false, + image: { + before: { mime: "image/svg+xml", data: Buffer.from(`${before}\n`).toString("base64") }, + after: { mime: "image/svg+xml", data: Buffer.from(`${after}\n`).toString("base64") }, + }, + }) + }) + + it("reuses converted image details while the snapshot is unchanged", async () => { + const { fetch } = recording([ + { file: "assets/banner.svg", patch: modifiedPatch, additions: 1, deletions: 1, status: "modified" }, + ]) + const source = createSessionDiffSource("s-image-cache", fetch, "/repo") + const first = await source.fetch() + const second = await source.fetch() + + expect(second.diffs).toBe(first.diffs) + }) + + it("marks binary snapshot images unavailable when their sides were not retained", async () => { + const { fetch } = recording([ + { file: "assets/banner.png", patch: "", additions: 0, deletions: 0, status: "modified" }, + ]) + const result = await createSessionDiffSource("s-image", fetch, "/repo").fetch() + + expect(result.diffs[0]?.image).toEqual({}) + }) + it("propagates errors from the underlying fetch", async () => { const { fetch } = recording(new Error("network down")) const source = createSessionDiffSource("s3", fetch) diff --git a/packages/kilo-vscode/tests/unit/extension-arch.test.ts b/packages/kilo-vscode/tests/unit/extension-arch.test.ts index 174989a2611..f2d07811517 100644 --- a/packages/kilo-vscode/tests/unit/extension-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/extension-arch.test.ts @@ -103,6 +103,17 @@ describe("Extension — package.json command sync", () => { `Commands without "kilo-code.new." prefix — use the namespaced form:\n` + bad.map((b) => ` - ${b}`).join("\n"), ).toEqual([]) }) + + it("scopes Agent Manager search to the panel and leaves the integrated terminal alone", () => { + const binding = pkg.contributes?.keybindings?.find( + (item: { command: string }) => item.command === "kilo-code.new.agentManager.search", + ) + expect(binding).toMatchObject({ + key: "ctrl+f", + mac: "cmd+f", + when: "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && !terminalFocus", + }) + }) }) // --------------------------------------------------------------------------- diff --git a/packages/kilo-vscode/tests/unit/format-keybinding.test.ts b/packages/kilo-vscode/tests/unit/format-keybinding.test.ts index 564b8a9ef3a..8320b94cd08 100644 --- a/packages/kilo-vscode/tests/unit/format-keybinding.test.ts +++ b/packages/kilo-vscode/tests/unit/format-keybinding.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "bun:test" -import { formatKeybinding } from "../../src/agent-manager/format-keybinding" +import { buildKeybindingMap, formatKeybinding } from "../../src/agent-manager/format-keybinding" describe("formatKeybinding", () => { describe("mac", () => { @@ -70,3 +70,11 @@ describe("formatKeybinding", () => { }) }) }) + +describe("buildKeybindingMap", () => { + it("maps the configurable Agent Manager search shortcut", () => { + const bindings = [{ command: "kilo-code.new.agentManager.search", key: "ctrl+f", mac: "cmd+f" }] + expect(buildKeybindingMap(bindings, true).search).toBe("⌘F") + expect(buildKeybindingMap(bindings, false).search).toBe("Ctrl+F") + }) +}) diff --git a/packages/kilo-vscode/tests/unit/ime-enter-key.test.ts b/packages/kilo-vscode/tests/unit/ime-enter-key.test.ts new file mode 100644 index 00000000000..219f84ed011 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/ime-enter-key.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from "bun:test" +import { isEnterKeyCommitNotIme } from "../../webview-ui/src/utils/ime-enter" + +describe("isEnterKeyCommitNotIme", () => { + it("is true for a normal Enter keydown", () => { + expect(isEnterKeyCommitNotIme({ key: "Enter", isComposing: false, keyCode: 13 })).toBe(true) + }) + + it("is false while isComposing is true", () => { + expect(isEnterKeyCommitNotIme({ key: "Enter", isComposing: true, keyCode: 13 })).toBe(false) + }) + + it("is false when keyCode is 229 (IME-processed key on Windows)", () => { + expect(isEnterKeyCommitNotIme({ key: "Enter", isComposing: false, keyCode: 229 })).toBe(false) + }) + + it("is false for non-Enter keys", () => { + expect(isEnterKeyCommitNotIme({ key: "a", isComposing: false, keyCode: 65 })).toBe(false) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/local-diff.test.ts b/packages/kilo-vscode/tests/unit/local-diff.test.ts index 67467db1e60..1e00c06470d 100644 --- a/packages/kilo-vscode/tests/unit/local-diff.test.ts +++ b/packages/kilo-vscode/tests/unit/local-diff.test.ts @@ -237,6 +237,28 @@ describe("diffSummary", () => { }) }) + it("loads binary-safe before and after data for image diffs", async () => { + await withRepo(async (dir, base) => { + const before = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x01]) + const after = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0xff]) + await fs.writeFile(path.join(dir, "banner.png"), before) + runSync(dir, ["add", "banner.png"]) + runSync(dir, ["commit", "-m", "add banner"]) + runSync(dir, ["branch", "-f", base]) + await fs.writeFile(path.join(dir, "banner.png"), after) + + const local = createLocalDiff(git()) + const summary = (await local.summary(dir, base)).find((entry) => entry.file === "banner.png") + const detail = await local.file(dir, base, "banner.png") + + expect(summary?.kind).toBe("image") + expect(summary?.summarized).toBe(true) + expect(detail?.summarized).toBe(false) + expect(detail?.image?.before?.data).toBe(before.toString("base64")) + expect(detail?.image?.after?.data).toBe(after.toString("base64")) + }) + }) + it("marks generated-like files via generatedLike flag", async () => { await withRepo(async (dir, base) => { await fs.mkdir(path.join(dir, "dist"), { recursive: true }) @@ -266,6 +288,20 @@ describe("diffFile", () => { }) }) + it("rejects image detail paths outside the repository", async () => { + await withRepo(async (dir, base) => { + const name = `${path.basename(dir)}-secret.png` + const secret = path.join(path.dirname(dir), name) + await fs.writeFile(secret, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00])) + try { + expect(await diffFile(git(), dir, base, `../${name}`)).toBeNull() + expect(await diffFile(git(), dir, base, secret)).toBeNull() + } finally { + await fs.rm(secret, { force: true }) + } + }) + }) + it("returns before/after/patch for a modified tracked file", async () => { await withRepo(async (dir, base) => { await fs.writeFile(path.join(dir, "seed.txt"), "seed\nmore\n") diff --git a/packages/kilo-vscode/tests/unit/provider-actions-save.test.ts b/packages/kilo-vscode/tests/unit/provider-actions-save.test.ts index 523ba6f94d8..a6499bab1c6 100644 --- a/packages/kilo-vscode/tests/unit/provider-actions-save.test.ts +++ b/packages/kilo-vscode/tests/unit/provider-actions-save.test.ts @@ -54,6 +54,9 @@ function createCtx(existing: ExistingGlobal = { disabled_providers: [] }, merged }), auth: async () => ({ data: {} }), }, + kilo: { + authStatus: async () => ({ data: { authenticated: false } }), + }, global: { config: { get: async () => ({ data: existing }), @@ -435,6 +438,9 @@ describe("fetchProviderData", () => { }), auth: async () => ({ data: {} }), }, + kilo: { + authStatus: async () => ({ data: { authenticated: false } }), + }, } as unknown as Parameters[0] const result = await fetchProviderData(client, "/tmp") @@ -444,6 +450,50 @@ describe("fetchProviderData", () => { expect("key" in item).toBe(false) }) + it("uses local Kilo auth status instead of profile availability", async () => { + const client = { + provider: { + list: async () => ({ + data: { + all: [{ id: "kilo", name: "Kilo Gateway", source: "custom", env: [], models: {} }], + connected: ["kilo"], + default: { kilo: "kilo-auto/frontier" }, + }, + }), + auth: async () => ({ data: {} }), + }, + kilo: { + authStatus: async () => ({ data: { authenticated: true, type: "oauth" } }), + }, + } as unknown as Parameters[0] + + const result = await fetchProviderData(client, "/tmp") + + expect(result.authStates).toEqual({ kilo: "oauth" }) + }) + + it("does not infer Kilo speech access without stored Gateway auth", async () => { + const client = { + provider: { + list: async () => ({ + data: { + all: [{ id: "kilo", name: "Kilo Gateway", source: "config", key: "configured", env: [], models: {} }], + connected: ["kilo"], + default: { kilo: "kilo-auto/frontier" }, + }, + }), + auth: async () => ({ data: {} }), + }, + kilo: { + authStatus: async () => ({ data: { authenticated: false } }), + }, + } as unknown as Parameters[0] + + const result = await fetchProviderData(client, "/tmp") + + expect(result.authStates).toEqual({}) + }) + it("retains stripped keys for providers with a configured baseURL", async () => { const client = { provider: { @@ -474,6 +524,9 @@ describe("fetchProviderData", () => { }), auth: async () => ({ data: {} }), }, + kilo: { + authStatus: async () => ({ data: { authenticated: false } }), + }, } as unknown as Parameters[0] const result = await fetchProviderData(client, "/tmp") diff --git a/packages/kilo-vscode/tests/unit/review-utils.test.ts b/packages/kilo-vscode/tests/unit/review-utils.test.ts new file mode 100644 index 00000000000..67793473010 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/review-utils.test.ts @@ -0,0 +1,29 @@ +import { afterEach, describe, expect, it, spyOn } from "bun:test" +import * as vscode from "vscode" +import { openFileInEditor } from "../../src/review-utils" + +const execute = spyOn(vscode.commands, "executeCommand") + +afterEach(() => { + execute.mockClear() +}) + +describe("openFileInEditor", () => { + it("delegates to vscode.open so custom image editors can handle the file", () => { + openFileInEditor("/repo/assets/banner.png", undefined, undefined, vscode.ViewColumn.One) + + expect(execute).toHaveBeenCalledTimes(1) + const [command, uri, options] = execute.mock.calls[0]! + expect(command).toBe("vscode.open") + expect((uri as vscode.Uri).fsPath).toBe("/repo/assets/banner.png") + expect(options).toEqual({ viewColumn: vscode.ViewColumn.One, preview: true }) + }) + + it("preserves line and column navigation for text editors", () => { + openFileInEditor("/repo/src/app.ts", 7, 3, vscode.ViewColumn.One) + + const options = execute.mock.calls[0]?.[2] as vscode.TextDocumentShowOptions + expect(options.selection?.start.line).toBe(6) + expect(options.selection?.start.character).toBe(2) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/sidebar-search.test.ts b/packages/kilo-vscode/tests/unit/sidebar-search.test.ts new file mode 100644 index 00000000000..9eeccde33b0 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/sidebar-search.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, it } from "bun:test" +import type { SectionState, SessionInfo, WorktreeState } from "../../webview-ui/src/types/messages" +import { buildSidebarSearch } from "../../webview-ui/agent-manager/sidebar-search" +import { buildShortcutCategories } from "../../webview-ui/agent-manager/shortcuts" + +const session = (id: string, title: string, updatedAt: string, parentID?: string): SessionInfo => ({ + id, + title, + parentID, + createdAt: "2026-06-01T00:00:00.000Z", + updatedAt, +}) + +const worktree = (id: string, branch: string, sectionId?: string): WorktreeState => ({ + id, + branch, + path: `/tmp/${id}`, + parentBranch: "main", + createdAt: "2026-06-01T00:00:00.000Z", + sectionId, +}) + +const section: SectionState = { + id: "section-polish", + name: "Polish", + color: "Blue", + order: 0, + collapsed: true, +} + +const build = (overrides?: Partial[0]>) => + buildSidebarSearch({ + worktrees: [ + { + worktree: worktree("wt-search", "feat/sidebar-search", section.id), + label: "Agent Manager search", + sessions: [ + session("busy-session", "Build grouped search", "2026-06-02T00:00:00.000Z"), + session("recent-session", "Review search UI", "2026-06-03T00:00:00.000Z"), + ], + }, + { + worktree: worktree("wt-other", "fix/other"), + label: "Other worktree", + sessions: [session("other-session", "Other session", "2026-06-04T00:00:00.000Z")], + }, + ], + sections: [section], + local: [session("local-session", "Local investigation", "2026-06-05T00:00:00.000Z")], + localLabel: "local", + localBranch: "main", + untitled: "Untitled", + pending: (id) => id.startsWith("pending:"), + status: (id) => (id === "busy-session" ? "busy" : "idle"), + busy: () => false, + localBusy: false, + ...overrides, + }) + +describe("buildSidebarSearch", () => { + it("indexes worktree sessions with section context and excludes non-root tabs", () => { + const items = build({ + worktrees: [ + { + worktree: worktree("wt-search", "feat/sidebar-search", section.id), + label: "Agent Manager search", + sessions: [ + session("session", "Build grouped search", "2026-06-02T00:00:00.000Z"), + session("pending:1", "New Session", "2026-06-03T00:00:00.000Z"), + session("child", "Subagent", "2026-06-04T00:00:00.000Z", "session"), + ], + }, + ], + local: [], + }) + + expect(items).toHaveLength(3) + expect(items[0]).toMatchObject({ + kind: "session", + sessionId: "session", + worktreeId: "wt-search", + meta: ["Polish", "Agent Manager search", "feat/sidebar-search"], + section: { id: section.id, color: "Blue", collapsed: true }, + }) + expect(items[0]?.search).toContain("Build grouped search Agent Manager search feat/sidebar-search Polish") + expect(items[1]).toMatchObject({ kind: "local", title: "local", meta: ["main"], count: 0 }) + expect(items[2]).toMatchObject({ kind: "worktree", worktreeId: "wt-search", count: 1, visible: false }) + }) + + it("ranks attention and progress before recency within each result group", () => { + const items = build() + + expect(items.map((item) => item.key)).toEqual([ + "session:busy-session", + "session:local-session", + "session:other-session", + "session:recent-session", + "worktree:wt-search", + "local", + "worktree:wt-other", + ]) + expect(items[0]).toMatchObject({ state: "busy", updatedAt: "2026-06-02T00:00:00.000Z" }) + expect(items[1]).toMatchObject({ location: "local", meta: ["local"] }) + expect(items[4]).toMatchObject({ state: "busy", updatedAt: "2026-06-03T00:00:00.000Z" }) + }) + + it("uses expanded sidebar visibility before recency as a tie-breaker", () => { + const items = build({ + worktrees: [ + { + worktree: worktree("wt-hidden", "feat/hidden", section.id), + label: "Same task hidden", + sessions: [session("hidden", "Same task", "2026-06-05T00:00:00.000Z")], + }, + { + worktree: worktree("wt-visible", "feat/visible"), + label: "Same task visible", + sessions: [session("visible", "Same task", "2026-06-01T00:00:00.000Z")], + }, + ], + local: [], + status: () => "idle", + }) + + expect(items.filter((item) => item.kind === "session").map((item) => item.sessionId)).toEqual(["visible", "hidden"]) + expect(items.find((item) => item.key === "session:hidden")).toMatchObject({ visible: false }) + expect(items.find((item) => item.key === "session:visible")).toMatchObject({ visible: true }) + }) + + it("avoids repeating a worktree label that matches the session title", () => { + const items = build({ + worktrees: [ + { + worktree: worktree("wt-owned", "feat/owned", section.id), + label: "Owned context", + sessions: [session("owned", "Owned context", "2026-06-02T00:00:00.000Z")], + }, + ], + local: [], + }) + + expect(items[0]).toMatchObject({ kind: "session", meta: ["Polish", "feat/owned"] }) + }) +}) + +describe("sidebar search shortcut", () => { + it("appears in the quick-switch keyboard shortcuts section", () => { + const categories = buildShortcutCategories({ search: "⌘F", jumpTo1: "⌘1" }, (key) => key) + expect(categories[0]?.shortcuts[0]).toEqual({ + label: "agentManager.sidebarSearch.label", + binding: "⌘F", + }) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/speech-to-text-availability.test.ts b/packages/kilo-vscode/tests/unit/speech-to-text-availability.test.ts index 2e6de323033..83848168963 100644 --- a/packages/kilo-vscode/tests/unit/speech-to-text-availability.test.ts +++ b/packages/kilo-vscode/tests/unit/speech-to-text-availability.test.ts @@ -6,17 +6,20 @@ import { import { DEFAULT_SPEECH_TO_TEXT_MODEL } from "../../src/speech-to-text/models" describe("speech-to-text availability", () => { - const providers = ["kilo"] - const profile = {} - - it("shows speech input by default when Kilo access exists", () => { - expect(canUseSpeechToText({}, providers, profile)).toBe(true) + it("shows speech input for stored Kilo credentials", () => { + expect(canUseSpeechToText({}, { kilo: "oauth" })).toBe(true) + expect(canUseSpeechToText({}, { kilo: "api" })).toBe(true) }) - it("hides speech input without a signed-in, enabled Kilo provider", () => { - expect(canUseSpeechToText({}, [], profile)).toBe(false) - expect(canUseSpeechToText({}, providers, null)).toBe(false) - expect(canUseSpeechToText({ disabled_providers: ["kilo"] }, providers, profile)).toBe(false) + it("hides speech input without usable Kilo credentials", () => { + expect(canUseSpeechToText({}, {})).toBe(false) + expect(canUseSpeechToText({}, { kilo: "wellknown" })).toBe(false) + }) + + it("honors enabled and disabled provider configuration", () => { + expect(canUseSpeechToText({ disabled_providers: ["kilo"] }, { kilo: "oauth" })).toBe(false) + expect(canUseSpeechToText({ enabled_providers: ["openai"] }, { kilo: "oauth" })).toBe(false) + expect(canUseSpeechToText({ enabled_providers: ["kilo"] }, { kilo: "oauth" })).toBe(true) }) it("normalizes configured and unknown transcription models", () => { diff --git a/packages/kilo-vscode/tests/unit/speech-to-text-models-sync.test.ts b/packages/kilo-vscode/tests/unit/speech-to-text-models-sync.test.ts index a09e20e8261..79a75d87256 100644 --- a/packages/kilo-vscode/tests/unit/speech-to-text-models-sync.test.ts +++ b/packages/kilo-vscode/tests/unit/speech-to-text-models-sync.test.ts @@ -6,7 +6,8 @@ import { } from "../../src/speech-to-text/models" describe("speech-to-text model catalog", () => { - it("keeps the first speech model as the fallback default", () => { + it("uses Whisper Large V3 Turbo as the fallback default", () => { + expect(DEFAULT_SPEECH_TO_TEXT_MODEL.id).toBe("openai/whisper-large-v3-turbo") expect(DEFAULT_SPEECH_TO_TEXT_MODEL.id).toBe(SPEECH_TO_TEXT_MODELS[0]?.id) }) diff --git a/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts b/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts index 52ac796d67a..ae28f0b4171 100644 --- a/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts +++ b/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts @@ -2,8 +2,13 @@ import { describe, expect, it, mock } from "bun:test" import { createRoot } from "solid-js" import type { ExtensionMessage, WebviewMessage } from "../../webview-ui/src/types/messages" +type Toast = { + actions?: Array<{ onClick: string | (() => void) }> +} + +const toasts: Toast[] = [] mock.module("@kilocode/kilo-ui/toast", () => ({ - showToast: () => undefined, + showToast: (toast: Toast) => toasts.push(toast), })) const { useSpeechToText } = await import("../../webview-ui/src/components/speech-to-text/useSpeechToText") @@ -11,6 +16,8 @@ const { useSpeechToText } = await import("../../webview-ui/src/components/speech function setup() { const sent: WebviewMessage[] = [] let handler: ((message: ExtensionMessage) => void) | undefined + let logins = 0 + toasts.length = 0 const root = createRoot((dispose) => ({ dispose, @@ -24,16 +31,37 @@ function setup() { } }, }, - { profileData: () => ({}), goToLogin: () => {} }, + { goToLogin: () => logins++ }, { t: (key) => key }, ), })) const fire = (message: ExtensionMessage) => handler?.(message) - return { ...root, fire, sent } + return { ...root, fire, sent, logins: () => logins } } describe("useSpeechToText", () => { + it("offers sign-in when stored credentials stop authenticating", () => { + const ctx = setup() + + ctx.speech.start({ model: "scribe", insert: () => {} }) + const start = ctx.sent[0] + if (start?.type !== "speechToTextStart") throw new Error("speech start message missing") + + ctx.fire({ + type: "speechToTextError", + requestId: start.requestId, + error: "Unauthorized", + code: "not_authenticated", + }) + const action = toasts[0]?.actions?.find((item) => typeof item.onClick === "function") + if (typeof action?.onClick === "function") action.onClick() + + expect(ctx.logins()).toBe(1) + expect(ctx.speech.error()).toBe("speechToText.error.loginRequired") + ctx.dispose() + }) + it("runs the stop completion after inserting a transcript", () => { const ctx = setup() const text: string[] = [] diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 7f9ee074816..a1a37b1314e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -101,7 +101,7 @@ import { reorderTabs, applyTabOrder, firstOrderedTitle } from "./tab-order" import { createTabOrderSync } from "./tab-order-sync" import { ConstrainDragYAxis } from "./sortable-tab" import { isTerminalTabId, createTerminalState, createTerminalHandlers, createTerminalMessageHandler } from "./terminal" -import { renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-rendering" +import { focusCurrentTab, renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-rendering" import { useTabScroll } from "./tab-scroll" import { DiffPanel } from "./DiffPanel" import { createRevertFile } from "./revert-file" @@ -110,7 +110,9 @@ import { ApplyDialog } from "./ApplyDialog" import { groupApplyConflicts } from "./apply-conflicts" import type { ReviewComment } from "../diff-viewer/review-comments" import { clearReviewComposer, createReviewComposer } from "../diff-viewer/review-annotations" -import { CurrentTabsMenu, createCurrentTabItems, focusCurrentTab } from "./CurrentTabsMenu" +import type { SidebarSearchMenuRef } from "./SidebarSearchMenu" +import { createSidebarSearch, type SidebarSearchItem } from "./sidebar-search" +import { WorktreeSectionActions } from "./WorktreeSectionActions" import { BranchSelect } from "../src/components/shared/BranchSelect" import { WorktreeItem } from "./WorktreeItem" import SectionHeader from "./SectionHeader" @@ -171,6 +173,7 @@ const defaultBindings: Record = { nextSession: isMac ? "⌘⌥↓" : "Ctrl+Alt+↓", previousTab: isMac ? "⌘⌥←" : "Ctrl+Alt+←", nextTab: isMac ? "⌘⌥→" : "Ctrl+Alt+→", + search: isMac ? "⌘F" : "Ctrl+F", showTerminal: isMac ? "⌘/" : "Ctrl+/", newTerminal: isMac ? "⌘⇧T" : "Ctrl+Shift+T", runScript: isMac ? "⌘E" : "Ctrl+E", @@ -197,6 +200,7 @@ const AgentManagerContent: Component = () => { const session = useSession() const vscode = useVSCode() const dialog = useDialog() + let sidebarSearchMenu: SidebarSearchMenuRef | undefined const [kb, setKb] = createSignal>(defaultBindings) @@ -964,6 +968,37 @@ const AgentManagerContent: Component = () => { return true } + const sidebarSearch = createSidebarSearch({ + worktrees: sortedWorktrees, + sections, + local: localSessions, + localBranch: repoBranch, + selection, + sessionId: session.currentSessionID, + statuses: session.allStatusMap, + permissions: session.permissions, + questions: session.questions, + label: worktreeLabel, + sessions: sessionsForWorktree, + pending: isPending, + busy: (id) => busyWorktrees().has(id) || (runStatuses()[id]?.state ?? "idle") !== "idle", + localBusy: isLocalBusy, + t, + }) + const focusSidebarSearchItem = (item: SidebarSearchItem) => { + if (item.section?.collapsed) + vscode.postMessage({ type: "agentManager.toggleSectionCollapsed", sectionId: item.section.id }) + setHistory(false) + if (item.kind === "local") return selectLocal() + if (item.kind === "worktree") return selectWorktree(item.worktreeId) + if (item.location === "local") selectLocal() + if (item.location === "worktree" && item.worktreeId) selectWorktree(item.worktreeId) + terms.setActiveId(undefined) + setReviewActive(false) + setActivePendingId(undefined) + session.selectSession(item.sessionId) + } + const cycleAgent = (direction: 1 | -1) => { const available = session.agents().filter((a) => a.mode !== "subagent" && !a.hidden) if (available.length <= 1) return @@ -990,7 +1025,13 @@ const AgentManagerContent: Component = () => { else if (msg.action === "sessionNext") navigate("down") else if (msg.action === "tabPrevious") navigateTab("left") else if (msg.action === "tabNext") navigateTab("right") - else if (msg.action === "showTerminal") { + else if (msg.action === "search") { + if (!sidebarCollapsed()) sidebarSearchMenu?.open() + else { + expandSidebar() + requestAnimationFrame(() => sidebarSearchMenu?.open()) + } + } else if (msg.action === "showTerminal") { // Cmd+/ opens the legacy VS Code integrated terminal for the // active session (or local). The new xterm tab affordance has // its own keybind (Cmd+Shift+T) so both coexist. @@ -2027,20 +2068,6 @@ const AgentManagerContent: Component = () => { activateTerminal: termHandlers.activate, }) - const tabMenuItems = createCurrentTabItems({ - tabIds, - tabLookup, - statusMap: session.allStatusMap, - permissions: session.permissions, - questions: session.questions, - visibleTabId, - terms, - reviewId: REVIEW_TAB_ID, - isTerminal: isTerminalTabId, - isPending, - t, - }) - // Close the currently active tab via keyboard shortcut. // If no tabs remain, fall through to close the selected worktree. const closeActiveTab = () => { @@ -2203,97 +2230,23 @@ const AgentManagerContent: Component = () => {
- -
-
- - - - - - - - - - - - {t("agentManager.dialog.configureWorktree")} - - {parseBindingTokens(kb().advancedWorktree ?? "").map((token) => ( - {token} - ))} - - - - newSection()}> - - {t("agentManager.worktree.newSection")} - - - - -
- - - - - - - - - {t("agentManager.worktree.setupScript")} - - - - - {t("agentManager.worktree.defaultBaseBranch")}: {repoDefaultBranch()} - - - - - -
-
+ (sidebarSearchMenu = value)} + onSelect={focusSidebarSearchItem} + onCreate={createWorktree} + onAdvanced={showAdvancedWorktreeDialog} + onSection={newSection} + onShortcuts={metrics.click("keyboard_shortcuts", "worktrees_header", handleShowKeyboardShortcuts)} + onSetup={setupScript} + onBranch={handleChangeDefaultBaseBranch} + />
{
-
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/CurrentTabsMenu.tsx b/packages/kilo-vscode/webview-ui/agent-manager/CurrentTabsMenu.tsx deleted file mode 100644 index b1369f75318..00000000000 --- a/packages/kilo-vscode/webview-ui/agent-manager/CurrentTabsMenu.tsx +++ /dev/null @@ -1,288 +0,0 @@ -/** @jsxImportSource solid-js */ - -import { For, Show, createEffect, createMemo, createSignal } from "solid-js" -import type { Accessor, Component } from "solid-js" -import { Popover } from "@kilocode/kilo-ui/popover" -import { Spinner } from "@kilocode/kilo-ui/spinner" -import { Tooltip } from "@kilocode/kilo-ui/tooltip" -import type { PermissionRequest, QuestionRequest, SessionInfo, SessionStatusInfo } from "../src/types/messages" -import type { TerminalStateControls } from "./terminal" - -interface CurrentTabItem { - id: string - title: string - status?: string - working: boolean - tone: "active" | "busy" | "waiting" | "idle" -} - -interface CurrentTabItemsDeps { - tabIds: Accessor - tabLookup: Accessor> - statusMap: Accessor> - permissions: Accessor - questions: Accessor - visibleTabId: Accessor - terms: TerminalStateControls - reviewId: string - isTerminal: (id: string) => boolean - isPending: (id: string) => boolean - t: (key: string) => string -} - -interface FocusTabDeps { - id: string - terms: TerminalStateControls - isTerminal: (id: string) => boolean - isPending: (id: string) => boolean - reviewId: string - reviewOpen: Accessor - setReviewOpen: (open: boolean) => void - setReviewActive: (active: boolean) => void - tabLookup: Accessor> - setActivePendingId: (id: string | undefined) => void - clearSession: () => void - selectSession: (id: string) => void - activateTerminal: (id: string) => void -} - -export function focusCurrentTab(deps: FocusTabDeps) { - if (deps.isTerminal(deps.id)) { - deps.activateTerminal(deps.id) - return - } - deps.terms.setActiveId(undefined) - if (deps.id === deps.reviewId) { - if (!deps.reviewOpen()) deps.setReviewOpen(true) - deps.setReviewActive(true) - return - } - const target = deps.tabLookup().get(deps.id) - if (!target) return - deps.setReviewActive(false) - if (deps.isPending(target.id)) { - deps.setActivePendingId(target.id) - deps.clearSession() - return - } - deps.setActivePendingId(undefined) - deps.selectSession(target.id) -} - -export const createCurrentTabItems = (deps: CurrentTabItemsDeps): Accessor => - createMemo(() => { - const statuses = deps.statusMap() - const perms = deps.permissions() - const qs = deps.questions() - return deps - .tabIds() - .map((id) => buildItem(id, statuses, perms, qs, deps)) - .filter((item): item is CurrentTabItem => item !== undefined) - }) - -const basicItem = (id: string, title: string, deps: CurrentTabItemsDeps): CurrentTabItem => ({ - id, - title, - working: false, - tone: id === deps.visibleTabId() ? "active" : "idle", -}) - -function buildReviewItem(id: string, deps: CurrentTabItemsDeps) { - return basicItem(id, deps.t("session.tab.review"), deps) -} - -function buildTerminalItem(id: string, deps: CurrentTabItemsDeps) { - const term = deps.terms.lookup().get(id) - if (!term) return undefined - return basicItem(id, term.title, deps) -} - -function buildPendingItem(tab: SessionInfo, deps: CurrentTabItemsDeps) { - return basicItem(tab.id, tab.title || deps.t("agentManager.session.newSession"), deps) -} - -function buildSessionItem( - tab: SessionInfo, - status: SessionStatusInfo | undefined, - blocked: boolean, - deps: CurrentTabItemsDeps, -) { - const working = !blocked && (status?.type === "busy" || status?.type === "retry") - return { - id: tab.id, - title: tab.title || deps.t("agentManager.session.untitled"), - status: statusLabel(tab.id, blocked, status, deps), - working, - tone: statusTone(tab.id, blocked, working, deps), - } satisfies CurrentTabItem -} - -function buildItem( - id: string, - statuses: Record, - perms: PermissionRequest[], - qs: QuestionRequest[], - deps: CurrentTabItemsDeps, -): CurrentTabItem | undefined { - if (id === deps.reviewId) return buildReviewItem(id, deps) - if (deps.isTerminal(id)) return buildTerminalItem(id, deps) - const tab = deps.tabLookup().get(id) - if (!tab) return undefined - if (deps.isPending(id)) return buildPendingItem(tab, deps) - const blocked = perms.some((p) => p.sessionID === id) || qs.some((q) => q.sessionID === id) - return buildSessionItem(tab, statuses[id], blocked, deps) -} - -function statusLabel(id: string, blocked: boolean, status: SessionStatusInfo | undefined, deps: CurrentTabItemsDeps) { - if (blocked) return deps.t("agentManager.tabsMenu.status.waiting") - if (status?.type === "retry") return deps.t("agentManager.tabsMenu.status.retry") - return undefined -} - -function statusTone(id: string, blocked: boolean, working: boolean, deps: CurrentTabItemsDeps): CurrentTabItem["tone"] { - if (id === deps.visibleTabId()) return "active" - if (blocked) return "waiting" - if (working) return "busy" - return "idle" -} - -interface CurrentTabsMenuProps { - items: Accessor - label: string - searchLabel: string - emptyLabel: string - activeId: Accessor - onSelect: (id: string) => void -} - -function SearchIcon() { - return ( - - ) -} - -export const CurrentTabsMenu: Component = (props) => { - const [search, setSearch] = createSignal("") - const [open, setOpen] = createSignal(false) - const [mark, setMark] = createSignal(0) - let input: HTMLInputElement | undefined - - const focus = () => { - input?.focus({ preventScroll: true }) - } - - createEffect(() => { - if (!open()) return - setSearch("") - queueMicrotask(focus) - }) - - const rows = createMemo(() => { - const q = search().trim().toLowerCase() - if (!q) return props.items() - return props.items().filter((item) => item.title.toLowerCase().includes(q)) - }) - - createEffect(() => { - if (!open()) return - search() - setMark(0) - }) - - const select = (id: string) => { - props.onSelect(id) - setOpen(false) - } - - const move = (dir: 1 | -1) => { - const len = rows().length - if (len === 0) return - setMark((prev) => (prev + dir + len) % len) - } - - const onKeyDown = (e: KeyboardEvent) => { - e.stopPropagation() - if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return - if (e.key === "ArrowDown") { - e.preventDefault() - move(1) - return - } - if (e.key === "ArrowUp") { - e.preventDefault() - move(-1) - return - } - if (e.key === "Enter") { - e.preventDefault() - const item = rows()[mark()] - if (item) select(item.id) - return - } - if (e.key === "Escape") { - e.preventDefault() - setOpen(false) - } - } - - return ( - - - - } - > - -
- 0} fallback={
{props.emptyLabel}
}> - - {(item) => ( - - )} - -
-
-
- ) -} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index 78be9646d81..c4f91269ad5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -57,6 +57,7 @@ import { DiffEndMarker } from "../diff-viewer/DiffEndMarker" import { VirtualDiffList } from "../diff-viewer/VirtualDiffList" import { treeOrder } from "../diff-viewer/file-tree-utils" import { isMarkdownFile, MarkdownDiffView } from "../diff-viewer/MarkdownDiffView" +import { ImageDiffView } from "../diff-viewer/ImageDiffView" import { createDiffRows, diffToken } from "../diff-viewer/diff-state" // --- Data model --- @@ -92,7 +93,7 @@ export const DiffPanel: Component = (props) => { const provider = useProvider() const { config } = useConfig() const speech = useSpeechToText(vscode, server, { t }) - const canUseSpeech = () => canUseSpeechToText(config(), provider.connected(), server.profileData()) + const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) const speechModel = () => selectedSpeechToTextModel(config()) const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) const sendAllKeybind = () => @@ -248,6 +249,15 @@ export const DiffPanel: Component = (props) => { ), ) + const request = (diff: WorktreeFileDiff) => { + if (!props.onRequestDiff || props.loadingFiles?.has(diff.file)) return + if (!isDiffExpandable(diff) || diff.summarized !== true) return + const value = diffToken(diff) + if (requested.get(diff.file) === value) return + requested.set(diff.file, value) + props.onRequestDiff(diff.file) + } + createEffect( on( () => [open(), props.diffs] as const, @@ -256,15 +266,10 @@ export const DiffPanel: Component = (props) => { for (const file of requested.keys()) { if (!files.has(file)) requested.delete(file) } - if (!props.onRequestDiff) return for (const file of next) { - if (props.loadingFiles?.has(file)) continue const diff = props.diffs.find((item) => item.file === file) - if (!diff || !isDiffExpandable(diff) || diff.summarized !== true) continue - const value = diffToken(diff) - if (requested.get(file) === value) continue - requested.set(file, value) - props.onRequestDiff(file) + if (!diff || diff.kind === "image") continue + request(diff) } }, { defer: true }, @@ -565,6 +570,10 @@ export const DiffPanel: Component = (props) => { const isLoadingDetail = () => props.loadingFiles?.has(diff.file) ?? false const fileCommentCount = () => (commentsByFile().get(diff.file) ?? []).length + createEffect(() => { + if (diff.kind === "image" && open().includes(diff.file)) request(diff) + }) + return ( = (props) => { + + {t("agentManager.review.image")} + {t("agentManager.review.largeFileCollapsed")} @@ -687,36 +699,43 @@ export const DiffPanel: Component = (props) => { } > - before={{ name: diff.file, contents: diff.before }} - after={{ name: diff.file, contents: diff.after }} - patch={diff.patch} - diffStyle={props.diffStyle ?? "unified"} - virtualized={shouldVirtualizeDiff(diff)} - annotations={annotationsForFile(diff.file)} - renderAnnotation={buildAnnotation} - enableGutterUtility={true} - onGutterUtilityClick={(result) => handleGutterClick(diff.file, result)} - onLineNumberClick={(event) => { - if (event.annotationSide === "deletions") return - props.onOpenFile?.(diff.file, event.lineNumber) - }} - /> + + before={{ name: diff.file, contents: diff.before }} + after={{ name: diff.file, contents: diff.after }} + patch={diff.patch} + diffStyle={props.diffStyle ?? "unified"} + virtualized={shouldVirtualizeDiff(diff)} + annotations={annotationsForFile(diff.file)} + renderAnnotation={buildAnnotation} + enableGutterUtility={true} + onGutterUtilityClick={(result) => handleGutterClick(diff.file, result)} + onLineNumberClick={(event) => { + if (event.annotationSide === "deletions") return + props.onOpenFile?.(diff.file, event.lineNumber) + }} + /> + } + > + handleGutterClick(diff.file, result)} + onLineNumberClick={(event) => { + if (event.annotationSide === "deletions") return + props.onOpenFile?.(diff.file, event.lineNumber) + }} + /> + } > - handleGutterClick(diff.file, result)} - onLineNumberClick={(event) => { - if (event.annotationSide === "deletions") return - props.onOpenFile?.(diff.file, event.lineNumber) - }} - /> + diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index 0685e6b72b1..25679aad3c0 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -103,7 +103,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran const [highlightedIndex, setHighlightedIndex] = createSignal(0) const [variant, setVariant] = createSignal(session.currentVariant()) const speech = useSpeechToText(vscode, server, { t }) - const canUseSpeech = () => canUseSpeechToText(config(), provider.connected(), server.profileData()) + const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) const speechModel = () => selectedSpeechToTextModel(config()) // Variant list for the currently selected model diff --git a/packages/kilo-vscode/webview-ui/agent-manager/SidebarSearchMenu.tsx b/packages/kilo-vscode/webview-ui/agent-manager/SidebarSearchMenu.tsx new file mode 100644 index 00000000000..7da9c9cb79f --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/SidebarSearchMenu.tsx @@ -0,0 +1,165 @@ +/** @jsxImportSource solid-js */ + +import { Show, createEffect, createSignal } from "solid-js" +import type { Accessor, Component } from "solid-js" +import { Icon } from "@kilocode/kilo-ui/icon" +import { List } from "@kilocode/kilo-ui/list" +import type { ListRef } from "@kilocode/kilo-ui/list" +import { Popover } from "@kilocode/kilo-ui/popover" +import { Spinner } from "@kilocode/kilo-ui/spinner" +import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip" +import { formatRelativeDate } from "../src/utils/date" +import { colorCss } from "./section-colors" +import type { SidebarSearchItem } from "./sidebar-search" + +export interface SidebarSearchMenuRef { + open: () => void +} + +interface SidebarSearchMenuProps { + items: Accessor + current: Accessor + labels: { search: string; scope: string; contexts: string; sessions: string; waiting: string; retry: string } + keybind: string + ref?: (value: SidebarSearchMenuRef) => void + onSelect: (item: SidebarSearchItem) => void + defaultOpen?: boolean + portal?: boolean +} + +export const SidebarSearchMenu: Component = (props) => { + const [open, setOpen] = createSignal(props.defaultOpen ?? false) + const [query, setQuery] = createSignal("") + const [notice, setNotice] = createSignal("") + let list: ListRef | undefined + let root: HTMLDivElement | undefined + + const focus = () => + queueMicrotask(() => { + list?.setFilter("") + root?.querySelector("input")?.focus({ preventScroll: true }) + }) + + createEffect(() => { + if (open()) focus() + }) + + const reveal = () => { + setOpen(true) + focus() + } + props.ref?.({ open: reveal }) + + const refocus = () => + requestAnimationFrame(() => window.dispatchEvent(new CustomEvent("focusPrompt", { detail: { restore: true } }))) + const close = (next: boolean) => { + setOpen(next) + if (!next) refocus() + } + + const select = (item: SidebarSearchItem) => { + setOpen(false) + props.onSelect(item) + refocus() + } + + return ( + + } + > + + + + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeSectionActions.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeSectionActions.tsx new file mode 100644 index 00000000000..0980bb060f2 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeSectionActions.tsx @@ -0,0 +1,139 @@ +/** @jsxImportSource solid-js */ + +import type { Accessor, Component } from "solid-js" +import { Show } from "solid-js" +import { DropdownMenu } from "@kilocode/kilo-ui/dropdown-menu" +import { Icon } from "@kilocode/kilo-ui/icon" +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip" +import type { LanguageContextValue } from "../src/context/language" +import { parseBindingTokens } from "./keybind-tokens" +import { SidebarSearchMenu, type SidebarSearchMenuRef } from "./SidebarSearchMenu" +import type { SidebarSearchItem } from "./sidebar-search" + +interface WorktreeSectionActionsProps { + items: Accessor + current: Accessor + bindings: Record + branch: string + git: boolean + loaded: boolean + t: LanguageContextValue["t"] + onRef: (ref: SidebarSearchMenuRef) => void + onSelect: (item: SidebarSearchItem) => void + onCreate: () => void + onAdvanced: () => void + onSection: () => void + onShortcuts: () => void + onSetup: () => void + onBranch: () => void +} + +export const WorktreeSectionActions: Component = (props) => ( +
+ + +
+ + + + + + + + + + + + {props.t("agentManager.dialog.configureWorktree")} + + {parseBindingTokens(props.bindings.advancedWorktree ?? "").map((token) => ( + {token} + ))} + + + + + + {props.t("agentManager.worktree.newSection")} + + + + +
+ + + + + + + + + {props.t("agentManager.worktree.setupScript")} + + + + + {props.t("agentManager.worktree.defaultBaseBranch")}: {props.branch} + + + + + +
+
+) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 7eea66cc458..995e0443cf9 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -1371,165 +1371,167 @@ body.am-wt-dragging-active * { padding-right: 4px; border-left: 1px solid var(--border-weak-base); } - -.am-tabs-menu-trigger { +.am-sidebar-search-trigger { display: inline-flex; - align-items: center; - justify-content: center; width: 24px; height: 24px; + flex-shrink: 0; + align-items: center; + justify-content: center; padding: 0; border: none; border-radius: var(--radius-sm); background: transparent; color: var(--text-weak); cursor: pointer; - flex-shrink: 0; } -.am-tabs-menu-trigger:hover, -[data-slot="popover-trigger"][data-expanded] .am-tabs-menu-trigger { +.am-sidebar-search-trigger:hover, +.am-sidebar-search-trigger[data-expanded] { background: var(--surface-inset-base-hover); color: var(--text-base); } -.am-tabs-search-icon { - width: 16px; - height: 16px; - fill: none; - stroke: currentColor; - stroke-width: 1.35; - stroke-linecap: round; - stroke-linejoin: round; -} - -.am-tabs-menu[data-component="popover-content"] { - width: 300px; - max-height: min(360px, calc(100vh - 64px)); +.am-sidebar-search-popover[data-component="popover-content"] { + width: min(360px, calc(100vw - 24px)); + max-height: min(480px, calc(100vh - 64px)); padding: 0; overflow: hidden; } -.am-tabs-menu [data-slot="popover-body"] { +.am-sidebar-search-popover [data-slot="popover-body"] { padding: 0; } -.am-tabs-menu-search { - display: flex; - align-items: center; - padding: 8px 12px; - border-bottom: 1px solid var(--border-base); +.am-sidebar-search [data-component="list"] { + max-height: min(440px, calc(100vh - 88px)); + gap: 0; + padding: 8px 0 0; +} + +.am-sidebar-search [data-slot="list-search-wrapper"] { + margin: 0 0 4px; + padding: 0 8px; +} + +.am-sidebar-search [data-slot="list-search"] { + background: var(--surface-inset-base); +} + +.am-sidebar-search [data-slot="list-scroll"] { + max-height: 360px; + gap: 2px; + padding: 0 4px; +} + +.am-sidebar-search [data-slot="list-group"]:last-child { + padding-bottom: 4px; +} + +.am-sidebar-search [data-slot="list-header"] { + padding: 7px 8px 4px; color: var(--text-weaker); + font-size: var(--kilo-font-size-10); + font-weight: 600; + letter-spacing: 0.06em; } -.am-tabs-menu-search-input { - flex: 1; - min-width: 0; - border: none; - background: transparent; - color: var(--text-base); - font-family: inherit; - font-size: var(--kilo-font-size-12); - outline: none; - user-select: text; +.am-sidebar-search [data-slot="list-item"] { + min-height: 46px; + padding: 6px 8px; } -.am-tabs-menu-search-input::placeholder { - color: var(--vscode-input-placeholderForeground, var(--text-weaker)); +.am-sidebar-search [data-slot="list-item"][data-active="true"], +.am-sidebar-search [data-slot="list-item"][data-selected="true"] { + background: color-mix(in srgb, var(--text-base) 10%, transparent); } -.am-tabs-menu-list { - max-height: 300px; - overflow-y: auto; - padding: 4px; -} - -.am-tabs-menu-item { +.am-sidebar-search-result { display: flex; + width: 100%; + min-width: 0; align-items: center; gap: 8px; - width: 100%; - min-height: 34px; - padding: 6px 10px; - border: none; - border-radius: var(--radius-sm); - background: transparent; - color: var(--text-base); - cursor: pointer; - font-family: inherit; - font-size: var(--kilo-font-size-12); - text-align: left; } -.am-tabs-menu-item:hover { - background: var(--surface-inset-base-hover); -} - -.am-tabs-menu-item-marked:not(.am-tabs-menu-item-active) { - background: var(--surface-inset-base-hover); -} - -.am-tabs-menu-item-active, -.am-tabs-menu-item-active:hover { - background: var(--surface-interactive-base); - color: var(--text-on-interactive-base); -} - -.am-tabs-menu-indicator { +.am-sidebar-search-icon { display: inline-flex; + width: 18px; + height: 18px; + flex-shrink: 0; align-items: center; justify-content: center; - width: 16px; - height: 16px; - flex-shrink: 0; + color: var(--text-weak); } -.am-tabs-menu-spinner { +.am-sidebar-search-spinner { width: 14px; height: 14px; } -.am-tabs-menu-status { - display: inline-flex; - align-items: center; - justify-content: flex-end; - flex-shrink: 0; - margin-left: auto; - color: var(--text-weaker); - font-size: var(--kilo-font-size-10); - line-height: 1; - text-transform: uppercase; -} - -.am-tabs-menu-status[data-tone="active"] { - color: var(--text-weak); -} - -.am-tabs-menu-status[data-tone="busy"] { - color: var(--vscode-testing-iconPassed, #34d399); -} - -.am-tabs-menu-status[data-tone="waiting"] { - color: #fbbf24; -} - -.am-tabs-menu-item-active .am-tabs-menu-status { - color: color-mix(in srgb, var(--text-on-interactive-base) 70%, transparent); -} - -.am-tabs-menu-title { - flex: 1; +.am-sidebar-search-copy { + display: flex; min-width: 0; + flex: 1; + flex-direction: column; + gap: 1px; + text-align: left; +} + +.am-sidebar-search-title, +.am-sidebar-search-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.am-tabs-menu-empty { - padding: 14px 10px; - color: var(--text-weaker); +.am-sidebar-search-title { + color: var(--text-base); font-size: var(--kilo-font-size-12); - text-align: center; + font-weight: 500; + line-height: 1.35; +} + +.am-sidebar-search-meta { + display: flex; + align-items: center; + gap: 5px; + color: var(--text-base); + font-size: var(--kilo-font-size-10); + font-weight: 400; + line-height: 1.35; +} + +.am-sidebar-search-swatch { + width: 6px; + height: 6px; + flex-shrink: 0; + border-radius: 50%; +} + +.am-sidebar-search-status, +.am-sidebar-search-count, +.am-sidebar-search-time { + flex-shrink: 0; + margin-left: auto; + color: var(--text-base); + font-size: var(--kilo-font-size-10); + font-weight: 400; +} + +.am-sidebar-search-status { + color: #fbbf24; + text-transform: uppercase; +} + +.am-sidebar-search-count { + display: inline-flex; + min-width: 18px; + height: 18px; + align-items: center; + justify-content: center; + border-radius: 9px; + background: var(--surface-inset-base); } /* Empty worktree state */ @@ -1961,6 +1963,78 @@ body.am-wt-dragging-active * { gap: 8px; } +.am-image-diff { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); + gap: 1px; + border-top: 1px solid var(--border-weak-base); + background: var(--border-weak-base); + cursor: auto; +} + +.am-image-diff-pane { + min-width: 0; + background: var(--vscode-editor-background, var(--surface-base)); +} + +.am-image-diff-label { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-height: 32px; + padding: 0 12px; + border-bottom: 1px solid var(--border-weak-base); + color: var(--text-weak); + font-size: var(--font-size-small); + font-weight: 600; +} + +.am-image-diff-canvas { + display: flex; + align-items: center; + justify-content: center; + min-height: 180px; + padding: 20px; + background-color: var(--vscode-editor-background, var(--surface-base)); + background-image: conic-gradient( + from 90deg at 1px 1px, + color-mix(in oklab, var(--text-base) 5%, transparent) 25%, + transparent 0 + ); + background-size: 16px 16px; + overflow: auto; +} + +.am-image-diff-canvas img { + display: block; + max-width: 100%; + max-height: 440px; + object-fit: contain; + box-shadow: 0 8px 28px color-mix(in oklab, black 20%, transparent); +} + +.am-diff-panel .am-image-diff-canvas img { + max-height: 320px; +} + +.am-image-diff-message, +.am-image-diff-unavailable { + color: var(--text-weak); + font-size: var(--font-size-small); + text-align: center; +} + +.am-image-diff-unavailable { + display: flex; + align-items: center; + justify-content: center; + min-height: 120px; + padding: 20px; + border-top: 1px solid var(--border-weak-base); + background: var(--vscode-editor-background, var(--surface-base)); +} + .am-markdown-diff { display: grid; grid-template-columns: minmax(0, 1fr); diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index 22b81da3b39..d85d268cfd5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -46,16 +46,17 @@ export const dict = { "agentManager.session.readonly": "جلسة للقراءة فقط", "agentManager.session.noSessions": "لا توجد جلسات مفتوحة", "agentManager.tab.close": "إغلاق", + "agentManager.tab.closeOthers": "إغلاق الآخرين", "agentManager.tab.closeTab": "إغلاق علامة التبويب", "agentManager.tab.forkSession": "تفريع الجلسة", "agentManager.tab.terminal": "الطرفية", "agentManager.tab.openTerminal": "فتح الطرفية", "agentManager.tab.newOptions": "المزيد من خيارات علامات التبويب الجديدة", - "agentManager.tabsMenu.label": "البحث في علامات التبويب المفتوحة", - "agentManager.tabsMenu.search": "البحث في علامات التبويب...", - "agentManager.tabsMenu.empty": "لا توجد علامات تبويب مطابقة", "agentManager.tabsMenu.status.waiting": "انتظار", "agentManager.tabsMenu.status.retry": "إعادة", + "agentManager.sidebarSearch.label": "البحث في Worktrees والجلسات", + "agentManager.sidebarSearch.scope": "يبحث في مساحة العمل المحلية والجلسات المحلية وWorktrees وجلساتها", + "agentManager.sidebarSearch.contexts": "محلي & WORKTREES", "agentManager.terminal.new": "علامة تبويب جديدة للمحطة الطرفية", "agentManager.terminal.ended": "انتهت المحطة الطرفية — أغلق علامة التبويب للإخفاء", @@ -180,6 +181,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} مطوي", "agentManager.review.collapsedWithLarge": "{{collapsed}} مطوي، {{large}} كبير", "agentManager.review.largeFileCollapsed": "ملف كبير (مطوي)", + "agentManager.review.image": "صورة", + "agentManager.review.imageBefore": "قبل", + "agentManager.review.imageAfter": "بعد", + "agentManager.review.imageTooLarge": "الصورة كبيرة جدًا بحيث يتعذر معاينتها ({{size}}).", + "agentManager.review.imageUnreadable": "تعذر عرض هذه الصورة.", + "agentManager.review.imageUnavailable": "معاينة الصورة غير متاحة في لقطة الجلسة هذه.", "agentManager.review.endOfLongDiff": "لقد وصلت إلى النهاية!", "agentManager.import.pullRequest": "طلب سحب", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index aec59b1bcf0..bbf01d966ec 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -46,16 +46,18 @@ export const dict = { "agentManager.session.readonly": "Sessão somente leitura", "agentManager.session.noSessions": "Nenhuma sessão aberta", "agentManager.tab.close": "Fechar", + "agentManager.tab.closeOthers": "Fechar outras", "agentManager.tab.closeTab": "Fechar aba", "agentManager.tab.forkSession": "Bifurcar sessão", "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Abrir Terminal", "agentManager.tab.newOptions": "Mais opções de nova aba", - "agentManager.tabsMenu.label": "Pesquisar abas abertas", - "agentManager.tabsMenu.search": "Pesquisar abas...", - "agentManager.tabsMenu.empty": "Nenhuma aba correspondente", "agentManager.tabsMenu.status.waiting": "Espera", "agentManager.tabsMenu.status.retry": "Repetir", + "agentManager.sidebarSearch.label": "Pesquisar Worktrees e sessões", + "agentManager.sidebarSearch.scope": + "Pesquisa o espaço de trabalho local, as sessões locais, os Worktrees e suas sessões", + "agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES", "agentManager.terminal.new": "Nova aba de terminal", "agentManager.terminal.ended": "terminal encerrado — feche a aba para dispensar", @@ -182,6 +184,13 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} recolhidos", "agentManager.review.collapsedWithLarge": "{{collapsed}} recolhidos, {{large}} grandes", "agentManager.review.largeFileCollapsed": "Arquivo grande (recolhido)", + "agentManager.review.image": "Imagem", + "agentManager.review.imageBefore": "Antes", + "agentManager.review.imageAfter": "Depois", + "agentManager.review.imageTooLarge": "A imagem é muito grande para ser visualizada ({{size}}).", + "agentManager.review.imageUnreadable": "Não foi possível renderizar esta imagem.", + "agentManager.review.imageUnavailable": + "A visualização da imagem não está disponível para este instantâneo da sessão.", "agentManager.review.endOfLongDiff": "Você chegou ao fim!", "agentManager.import.pullRequest": "Solicitação de extração", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index d3b3ca7953b..b35b51e5668 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -46,16 +46,17 @@ export const dict = { "agentManager.session.readonly": "Sesija samo za čitanje", "agentManager.session.noSessions": "Nema otvorenih sesija", "agentManager.tab.close": "Zatvori", + "agentManager.tab.closeOthers": "Zatvori ostale", "agentManager.tab.closeTab": "Zatvori karticu", "agentManager.tab.forkSession": "Razdvoji sesiju", "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Otvori Terminal", "agentManager.tab.newOptions": "Više opcija nove kartice", - "agentManager.tabsMenu.label": "Pretraži otvorene kartice", - "agentManager.tabsMenu.search": "Pretraži kartice...", - "agentManager.tabsMenu.empty": "Nema podudarnih kartica", "agentManager.tabsMenu.status.waiting": "Čeka", "agentManager.tabsMenu.status.retry": "Pokušaj", + "agentManager.sidebarSearch.label": "Pretraži Worktree-ove i sesije", + "agentManager.sidebarSearch.scope": "Pretražuje lokalni radni prostor, lokalne sesije, Worktree-ove i njihove sesije", + "agentManager.sidebarSearch.contexts": "LOKALNO & WORKTREES", "agentManager.terminal.new": "Nova kartica terminala", "agentManager.terminal.ended": "terminal je završen — zatvorite karticu da biste odbacili", @@ -183,6 +184,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} sažeto", "agentManager.review.collapsedWithLarge": "{{collapsed}} sažeto, {{large}} velikih", "agentManager.review.largeFileCollapsed": "Velika datoteka (sažeto)", + "agentManager.review.image": "Slika", + "agentManager.review.imageBefore": "Prije", + "agentManager.review.imageAfter": "Poslije", + "agentManager.review.imageTooLarge": "Slika je prevelika za pregled ({{size}}).", + "agentManager.review.imageUnreadable": "Ova slika se nije mogla prikazati.", + "agentManager.review.imageUnavailable": "Pregled slike nije dostupan za ovaj snimak sesije.", "agentManager.review.endOfLongDiff": "Došli ste do kraja!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index 194ad09be4a..731e99591b0 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -46,16 +46,19 @@ export const dict = { "agentManager.session.readonly": "Skrivebeskyttet session", "agentManager.session.noSessions": "Ingen åbne sessioner", "agentManager.tab.close": "Luk", + "agentManager.tab.closeOthers": "Luk andre", "agentManager.tab.closeTab": "Luk fane", "agentManager.tab.forkSession": "Forgren session", "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Åbn Terminal", "agentManager.tab.newOptions": "Flere nye faneindstillinger", - "agentManager.tabsMenu.label": "Søg i åbne faner", - "agentManager.tabsMenu.search": "Søg faner...", - "agentManager.tabsMenu.empty": "Ingen matchende faner", + "agentManager.tabsMenu.status.waiting": "Venter", "agentManager.tabsMenu.status.retry": "Igen", + "agentManager.sidebarSearch.label": "Søg i Worktrees og sessioner", + "agentManager.sidebarSearch.scope": + "Søger i det lokale arbejdsområde, lokale sessioner, Worktrees og deres sessioner", + "agentManager.sidebarSearch.contexts": "LOKAL & WORKTREES", "agentManager.terminal.new": "Ny terminalfane", "agentManager.terminal.ended": "terminal afsluttet — luk fanen for at fjerne", @@ -182,6 +185,13 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} foldet sammen", "agentManager.review.collapsedWithLarge": "{{collapsed}} foldet sammen, {{large}} store", "agentManager.review.largeFileCollapsed": "Stor fil (sammenklappet)", + "agentManager.review.image": "Billede", + "agentManager.review.imageBefore": "Før", + "agentManager.review.imageAfter": "Efter", + "agentManager.review.imageTooLarge": "Billedet er for stort til at kunne forhåndsvises ({{size}}).", + "agentManager.review.imageUnreadable": "Dette billede kunne ikke gengives.", + "agentManager.review.imageUnavailable": + "Forhåndsvisning af billedet er ikke tilgængelig for dette snapshot af sessionen.", "agentManager.review.endOfLongDiff": "Du nåede slutningen!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index f6643237ba9..52e5f726d10 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -46,16 +46,18 @@ export const dict = { "agentManager.session.readonly": "Schreibgeschützte Sitzung", "agentManager.session.noSessions": "Keine Sitzungen geöffnet", "agentManager.tab.close": "Schließen", + "agentManager.tab.closeOthers": "Andere schließen", "agentManager.tab.closeTab": "Tab schließen", "agentManager.tab.forkSession": "Sitzung verzweigen", "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Terminal öffnen", "agentManager.tab.newOptions": "Weitere Tab-Optionen", - "agentManager.tabsMenu.label": "Offene Tabs suchen", - "agentManager.tabsMenu.search": "Tabs suchen...", - "agentManager.tabsMenu.empty": "Keine passenden Tabs", "agentManager.tabsMenu.status.waiting": "Warten", "agentManager.tabsMenu.status.retry": "Erneut", + "agentManager.sidebarSearch.label": "Worktrees und Sitzungen durchsuchen", + "agentManager.sidebarSearch.scope": + "Durchsucht den lokalen Arbeitsbereich, lokale Sitzungen, Worktrees und deren Sitzungen", + "agentManager.sidebarSearch.contexts": "LOKAL & WORKTREES", "agentManager.terminal.new": "Neuer Terminal-Tab", "agentManager.terminal.ended": "Terminal beendet — Tab schließen zum Verwerfen", @@ -183,6 +185,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} eingeklappt", "agentManager.review.collapsedWithLarge": "{{collapsed}} eingeklappt, {{large}} groß", "agentManager.review.largeFileCollapsed": "Große Datei (eingeklappt)", + "agentManager.review.image": "Bild", + "agentManager.review.imageBefore": "Vorher", + "agentManager.review.imageAfter": "Nachher", + "agentManager.review.imageTooLarge": "Das Bild ist zu groß für eine Vorschau ({{size}}).", + "agentManager.review.imageUnreadable": "Dieses Bild konnte nicht dargestellt werden.", + "agentManager.review.imageUnavailable": "Für diesen Sitzungssnapshot ist keine Bildvorschau verfügbar.", "agentManager.review.endOfLongDiff": "Du hast das Ende erreicht!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index cc94798638a..7dbaeb1b73a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -50,16 +50,17 @@ export const dict = { "agentManager.session.noSessions": "No sessions open", "agentManager.tab.close": "Close", + "agentManager.tab.closeOthers": "Close Others", "agentManager.tab.closeTab": "Close tab", "agentManager.tab.forkSession": "Fork Session", "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Open Terminal", "agentManager.tab.newOptions": "More new-tab options", - "agentManager.tabsMenu.label": "Search and switch tabs", - "agentManager.tabsMenu.search": "Search tabs...", - "agentManager.tabsMenu.empty": "No matching tabs", "agentManager.tabsMenu.status.waiting": "Wait", "agentManager.tabsMenu.status.retry": "Retry", + "agentManager.sidebarSearch.label": "Search worktrees and sessions", + "agentManager.sidebarSearch.scope": "Searches the local workspace, local sessions, worktrees, and their sessions", + "agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES", "agentManager.terminal.new": "New Terminal Tab", "agentManager.terminal.ended": "terminal ended — close tab to dismiss", @@ -187,6 +188,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} collapsed", "agentManager.review.collapsedWithLarge": "{{collapsed}} collapsed, {{large}} large", "agentManager.review.largeFileCollapsed": "Large file (collapsed)", + "agentManager.review.image": "image", + "agentManager.review.imageBefore": "Before", + "agentManager.review.imageAfter": "After", + "agentManager.review.imageTooLarge": "Image is too large to preview ({{size}}).", + "agentManager.review.imageUnreadable": "This image could not be rendered.", + "agentManager.review.imageUnavailable": "Image preview is unavailable for this session snapshot.", "agentManager.review.endOfLongDiff": "You made it to the end!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index bfddc7036fd..a2dbbba2bc1 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -46,16 +46,18 @@ export const dict = { "agentManager.session.readonly": "Sesión de solo lectura", "agentManager.session.noSessions": "No hay sesiones abiertas", "agentManager.tab.close": "Cerrar", + "agentManager.tab.closeOthers": "Cerrar otras", "agentManager.tab.closeTab": "Cerrar pestaña", "agentManager.tab.forkSession": "Bifurcar sesión", "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Abrir Terminal", "agentManager.tab.newOptions": "Más opciones de nueva pestaña", - "agentManager.tabsMenu.label": "Buscar pestañas abiertas", - "agentManager.tabsMenu.search": "Buscar pestañas...", - "agentManager.tabsMenu.empty": "No hay pestañas coincidentes", "agentManager.tabsMenu.status.waiting": "Espera", "agentManager.tabsMenu.status.retry": "Reintento", + "agentManager.sidebarSearch.label": "Buscar Worktrees y sesiones", + "agentManager.sidebarSearch.scope": + "Busca en el espacio de trabajo local, las sesiones locales, los Worktrees y sus sesiones", + "agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES", "agentManager.terminal.new": "Nueva pestaña de terminal", "agentManager.terminal.ended": "terminal finalizado — cierra la pestaña para descartar", @@ -182,6 +184,13 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} contraídos", "agentManager.review.collapsedWithLarge": "{{collapsed}} contraídos, {{large}} grandes", "agentManager.review.largeFileCollapsed": "Archivo grande (contraído)", + "agentManager.review.image": "Imagen", + "agentManager.review.imageBefore": "Antes", + "agentManager.review.imageAfter": "Después", + "agentManager.review.imageTooLarge": "La imagen es demasiado grande para previsualizarla ({{size}}).", + "agentManager.review.imageUnreadable": "No se pudo renderizar esta imagen.", + "agentManager.review.imageUnavailable": + "La vista previa de la imagen no está disponible para esta instantánea de la sesión.", "agentManager.review.endOfLongDiff": "¡Llegaste al final!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index 435308590a4..21fe585437f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -46,16 +46,18 @@ export const dict = { "agentManager.session.readonly": "Session en lecture seule", "agentManager.session.noSessions": "Aucune session ouverte", "agentManager.tab.close": "Fermer", + "agentManager.tab.closeOthers": "Fermer les autres", "agentManager.tab.closeTab": "Fermer l'onglet", "agentManager.tab.forkSession": "Dupliquer la session", "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Ouvrir le Terminal", "agentManager.tab.newOptions": "Plus d'options de nouvel onglet", - "agentManager.tabsMenu.label": "Rechercher les onglets ouverts", - "agentManager.tabsMenu.search": "Rechercher des onglets...", - "agentManager.tabsMenu.empty": "Aucun onglet correspondant", "agentManager.tabsMenu.status.waiting": "Attente", "agentManager.tabsMenu.status.retry": "Réessai", + "agentManager.sidebarSearch.label": "Rechercher des Worktrees et des sessions", + "agentManager.sidebarSearch.scope": + "Recherche dans l'espace de travail local, les sessions locales, les Worktrees et leurs sessions", + "agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES", "agentManager.terminal.new": "Nouvel onglet de terminal", "agentManager.terminal.ended": "terminal terminé — fermez l'onglet pour ignorer", @@ -183,6 +185,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} repliés", "agentManager.review.collapsedWithLarge": "{{collapsed}} repliés, {{large}} volumineux", "agentManager.review.largeFileCollapsed": "Fichier volumineux (replié)", + "agentManager.review.image": "Image", + "agentManager.review.imageBefore": "Avant", + "agentManager.review.imageAfter": "Après", + "agentManager.review.imageTooLarge": "L’image est trop volumineuse pour être prévisualisée ({{size}}).", + "agentManager.review.imageUnreadable": "Cette image n’a pas pu être affichée.", + "agentManager.review.imageUnavailable": "L’aperçu de l’image n’est pas disponible pour cet instantané de session.", "agentManager.review.endOfLongDiff": "Vous êtes arrivé à la fin !", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts index 24e5636bb09..f73e589d6c9 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -50,16 +50,18 @@ export const dict = { "agentManager.session.noSessions": "Nessuna sessione aperta", "agentManager.tab.close": "Chiudi", + "agentManager.tab.closeOthers": "Chiudi le altre", "agentManager.tab.closeTab": "Chiudi scheda", "agentManager.tab.forkSession": "Forka sessione", "agentManager.tab.terminal": "Terminale", "agentManager.tab.openTerminal": "Apri terminale", "agentManager.tab.newOptions": "Altre opzioni nuova scheda", - "agentManager.tabsMenu.label": "Cerca e cambia scheda", - "agentManager.tabsMenu.search": "Cerca schede...", - "agentManager.tabsMenu.empty": "Nessuna scheda corrispondente", "agentManager.tabsMenu.status.waiting": "Attendi", "agentManager.tabsMenu.status.retry": "Riprova", + "agentManager.sidebarSearch.label": "Cerca Worktree e sessioni", + "agentManager.sidebarSearch.scope": + "Cerca nell'area di lavoro locale, nelle sessioni locali, nei Worktree e nelle relative sessioni", + "agentManager.sidebarSearch.contexts": "LOCALE & WORKTREE", "agentManager.terminal.new": "Nuova scheda terminale", "agentManager.terminal.ended": "terminale terminato - chiudi la scheda per nasconderlo", @@ -188,6 +190,13 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} compressi", "agentManager.review.collapsedWithLarge": "{{collapsed}} compressi, {{large}} grandi", "agentManager.review.largeFileCollapsed": "File grande (compresso)", + "agentManager.review.image": "Immagine", + "agentManager.review.imageBefore": "Prima", + "agentManager.review.imageAfter": "Dopo", + "agentManager.review.imageTooLarge": "L'immagine è troppo grande per visualizzare l'anteprima ({{size}}).", + "agentManager.review.imageUnreadable": "Non è stato possibile visualizzare questa immagine.", + "agentManager.review.imageUnavailable": + "L'anteprima dell'immagine non è disponibile per questa istantanea della sessione.", "agentManager.review.endOfLongDiff": "Sei arrivato alla fine!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index 4d37c64e2e3..0bce98ce906 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -46,16 +46,18 @@ export const dict = { "agentManager.session.readonly": "読み取り専用セッション", "agentManager.session.noSessions": "開いているセッションがありません", "agentManager.tab.close": "閉じる", + "agentManager.tab.closeOthers": "他を閉じる", "agentManager.tab.closeTab": "タブを閉じる", "agentManager.tab.forkSession": "セッションをフォーク", "agentManager.tab.terminal": "ターミナル", "agentManager.tab.openTerminal": "ターミナルを開く", "agentManager.tab.newOptions": "新しいタブのその他のオプション", - "agentManager.tabsMenu.label": "開いているタブを検索", - "agentManager.tabsMenu.search": "タブを検索...", - "agentManager.tabsMenu.empty": "一致するタブがありません", "agentManager.tabsMenu.status.waiting": "待機", "agentManager.tabsMenu.status.retry": "再試行", + "agentManager.sidebarSearch.label": "Worktreeとセッションを検索", + "agentManager.sidebarSearch.scope": + "ローカルワークスペース、ローカルセッション、Worktree、および各Worktreeのセッションを検索", + "agentManager.sidebarSearch.contexts": "ローカル & WORKTREES", "agentManager.terminal.new": "新しいターミナルタブ", "agentManager.terminal.ended": "ターミナルが終了しました — タブを閉じて破棄", @@ -182,6 +184,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} 件折りたたみ", "agentManager.review.collapsedWithLarge": "{{collapsed}} 件折りたたみ、{{large}} 件がサイズ大", "agentManager.review.largeFileCollapsed": "大きなファイル(折りたたみ)", + "agentManager.review.image": "画像", + "agentManager.review.imageBefore": "変更前", + "agentManager.review.imageAfter": "変更後", + "agentManager.review.imageTooLarge": "画像が大きすぎるためプレビューできません({{size}})。", + "agentManager.review.imageUnreadable": "この画像を表示できませんでした。", + "agentManager.review.imageUnavailable": "このセッションスナップショットでは画像プレビューを利用できません。", "agentManager.review.endOfLongDiff": "最後まで到達しました!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index f7d7e9a2d92..51c0fa64618 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -46,16 +46,17 @@ export const dict = { "agentManager.session.readonly": "읽기 전용 세션", "agentManager.session.noSessions": "열린 세션 없음", "agentManager.tab.close": "닫기", + "agentManager.tab.closeOthers": "다른 탭 닫기", "agentManager.tab.closeTab": "탭 닫기", "agentManager.tab.forkSession": "세션 포크", "agentManager.tab.terminal": "터미널", "agentManager.tab.openTerminal": "터미널 열기", "agentManager.tab.newOptions": "더 많은 새 탭 옵션", - "agentManager.tabsMenu.label": "열린 탭 검색", - "agentManager.tabsMenu.search": "탭 검색...", - "agentManager.tabsMenu.empty": "일치하는 탭 없음", "agentManager.tabsMenu.status.waiting": "대기", "agentManager.tabsMenu.status.retry": "재시도", + "agentManager.sidebarSearch.label": "Worktree 및 세션 검색", + "agentManager.sidebarSearch.scope": "로컬 워크스페이스, 로컬 세션, Worktree 및 각 Worktree의 세션 검색", + "agentManager.sidebarSearch.contexts": "로컬 & WORKTREES", "agentManager.terminal.new": "새 터미널 탭", "agentManager.terminal.ended": "터미널 종료됨 — 탭을 닫아 해제", @@ -182,6 +183,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}}개 접힘", "agentManager.review.collapsedWithLarge": "{{collapsed}}개 접힘, {{large}}개 대용량", "agentManager.review.largeFileCollapsed": "큰 파일(접힘)", + "agentManager.review.image": "이미지", + "agentManager.review.imageBefore": "변경 전", + "agentManager.review.imageAfter": "변경 후", + "agentManager.review.imageTooLarge": "이미지가 너무 커서 미리 볼 수 없습니다({{size}}).", + "agentManager.review.imageUnreadable": "이 이미지를 렌더링할 수 없습니다.", + "agentManager.review.imageUnavailable": "이 세션 스냅샷에서는 이미지 미리보기를 사용할 수 없습니다.", "agentManager.review.endOfLongDiff": "끝까지 도달했습니다!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts index 713c7349005..dc06b9a8d2a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -50,16 +50,17 @@ export const dict = { "agentManager.session.noSessions": "Geen open sessies", "agentManager.tab.close": "Sluiten", + "agentManager.tab.closeOthers": "Andere sluiten", "agentManager.tab.closeTab": "Tabblad sluiten", "agentManager.tab.forkSession": "Sessie forken", "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Terminal openen", "agentManager.tab.newOptions": "Meer opties voor nieuwe tabblad", - "agentManager.tabsMenu.label": "Open tabbladen zoeken", - "agentManager.tabsMenu.search": "Tabbladen zoeken...", - "agentManager.tabsMenu.empty": "Geen overeenkomende tabbladen", "agentManager.tabsMenu.status.waiting": "Wacht", "agentManager.tabsMenu.status.retry": "Opnieuw", + "agentManager.sidebarSearch.label": "Worktrees en sessies doorzoeken", + "agentManager.sidebarSearch.scope": "Doorzoekt de lokale werkruimte, lokale sessies, Worktrees en hun sessies", + "agentManager.sidebarSearch.contexts": "LOKAAL & WORKTREES", "agentManager.terminal.new": "Nieuw terminaltabblad", "agentManager.terminal.ended": "terminal beëindigd — sluit tabblad om te negeren", @@ -188,6 +189,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} ingeklapt", "agentManager.review.collapsedWithLarge": "{{collapsed}} ingeklapt, {{large}} groot", "agentManager.review.largeFileCollapsed": "Groot bestand (ingeklapt)", + "agentManager.review.image": "Afbeelding", + "agentManager.review.imageBefore": "Voor", + "agentManager.review.imageAfter": "Na", + "agentManager.review.imageTooLarge": "Afbeelding is te groot voor een voorbeeldweergave ({{size}}).", + "agentManager.review.imageUnreadable": "Deze afbeelding kon niet worden weergegeven.", + "agentManager.review.imageUnavailable": "Afbeeldingsvoorbeeld is niet beschikbaar voor deze sessiesnapshot.", "agentManager.review.endOfLongDiff": "Je hebt het einde bereikt!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index a412c141caa..4876091f707 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -46,16 +46,17 @@ export const dict = { "agentManager.session.readonly": "Skrivebeskyttet økt", "agentManager.session.noSessions": "Ingen åpne økter", "agentManager.tab.close": "Lukk", + "agentManager.tab.closeOthers": "Lukk andre", "agentManager.tab.closeTab": "Lukk fane", "agentManager.tab.forkSession": "Forgrein økt", "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Åpne Terminal", "agentManager.tab.newOptions": "Flere alternativer for ny fane", - "agentManager.tabsMenu.label": "Søk i åpne faner", - "agentManager.tabsMenu.search": "Søk i faner...", - "agentManager.tabsMenu.empty": "Ingen treffende faner", "agentManager.tabsMenu.status.waiting": "Venter", "agentManager.tabsMenu.status.retry": "Igjen", + "agentManager.sidebarSearch.label": "Søk i Worktrees og økter", + "agentManager.sidebarSearch.scope": "Søker i det lokale arbeidsområdet, lokale økter, Worktrees og øktene deres", + "agentManager.sidebarSearch.contexts": "LOKAL & WORKTREES", "agentManager.terminal.new": "Ny terminalfane", "agentManager.terminal.ended": "terminal avsluttet — lukk fanen for å avvise", @@ -182,6 +183,13 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} kollapset", "agentManager.review.collapsedWithLarge": "{{collapsed}} kollapset, {{large}} store", "agentManager.review.largeFileCollapsed": "Stor fil (sammenfoldet)", + "agentManager.review.image": "Bilde", + "agentManager.review.imageBefore": "Før", + "agentManager.review.imageAfter": "Etter", + "agentManager.review.imageTooLarge": "Bildet er for stort til å forhåndsvises ({{size}}).", + "agentManager.review.imageUnreadable": "Dette bildet kunne ikke gjengis.", + "agentManager.review.imageUnavailable": + "Forhåndsvisning av bildet er ikke tilgjengelig for dette øyeblikksbildet av økten.", "agentManager.review.endOfLongDiff": "Du nådde slutten!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index f985f44198b..4d5248932ad 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -46,16 +46,18 @@ export const dict = { "agentManager.session.readonly": "Sesja tylko do odczytu", "agentManager.session.noSessions": "Brak otwartych sesji", "agentManager.tab.close": "Zamknij", + "agentManager.tab.closeOthers": "Zamknij pozostałe", "agentManager.tab.closeTab": "Zamknij kartę", "agentManager.tab.forkSession": "Rozgałęź sesję", "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Otwórz Terminal", "agentManager.tab.newOptions": "Więcej opcji nowej karty", - "agentManager.tabsMenu.label": "Szukaj otwartych kart", - "agentManager.tabsMenu.search": "Szukaj kart...", - "agentManager.tabsMenu.empty": "Brak pasujących kart", "agentManager.tabsMenu.status.waiting": "Czeka", "agentManager.tabsMenu.status.retry": "Ponów", + "agentManager.sidebarSearch.label": "Wyszukaj Worktree i sesje", + "agentManager.sidebarSearch.scope": + "Przeszukuje lokalny obszar roboczy, lokalne sesje, Worktree i przypisane do nich sesje", + "agentManager.sidebarSearch.contexts": "LOKALNE & WORKTREES", "agentManager.terminal.new": "Nowa karta terminala", "agentManager.terminal.ended": "terminal zakończony — zamknij kartę, aby zamknąć", @@ -181,6 +183,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} zwiniętych", "agentManager.review.collapsedWithLarge": "{{collapsed}} zwiniętych, {{large}} dużych", "agentManager.review.largeFileCollapsed": "Duży plik (zwinięty)", + "agentManager.review.image": "Obraz", + "agentManager.review.imageBefore": "Przed", + "agentManager.review.imageAfter": "Po", + "agentManager.review.imageTooLarge": "Obraz jest zbyt duży, aby wyświetlić podgląd ({{size}}).", + "agentManager.review.imageUnreadable": "Nie można wyświetlić tego obrazu.", + "agentManager.review.imageUnavailable": "Podgląd obrazu jest niedostępny dla tej migawki sesji.", "agentManager.review.endOfLongDiff": "Dotarłeś do końca!", "agentManager.import.pullRequest": "Prośba o scalenie", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index 4b5a8c9ee37..d6cf04b86d2 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -46,16 +46,18 @@ export const dict = { "agentManager.session.readonly": "Сессия только для чтения", "agentManager.session.noSessions": "Нет открытых сессий", "agentManager.tab.close": "Закрыть", + "agentManager.tab.closeOthers": "Закрыть остальные", "agentManager.tab.closeTab": "Закрыть вкладку", "agentManager.tab.forkSession": "Ответвить сессию", "agentManager.tab.terminal": "Терминал", "agentManager.tab.openTerminal": "Открыть терминал", "agentManager.tab.newOptions": "Другие параметры новой вкладки", - "agentManager.tabsMenu.label": "Искать открытые вкладки", - "agentManager.tabsMenu.search": "Поиск вкладок...", - "agentManager.tabsMenu.empty": "Нет подходящих вкладок", "agentManager.tabsMenu.status.waiting": "Ожидание", "agentManager.tabsMenu.status.retry": "Повтор", + "agentManager.sidebarSearch.label": "Поиск по Worktree и сессиям", + "agentManager.sidebarSearch.scope": + "Поиск локального рабочего пространства, локальных сессий, Worktree и связанных с ними сессий", + "agentManager.sidebarSearch.contexts": "ЛОКАЛЬНЫЙ & WORKTREES", "agentManager.terminal.new": "Новая вкладка терминала", "agentManager.terminal.ended": "терминал завершен — закройте вкладку, чтобы скрыть", @@ -182,6 +184,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} свернуто", "agentManager.review.collapsedWithLarge": "{{collapsed}} свернуто, {{large}} больших", "agentManager.review.largeFileCollapsed": "Большой файл (свернут)", + "agentManager.review.image": "Изображение", + "agentManager.review.imageBefore": "До", + "agentManager.review.imageAfter": "После", + "agentManager.review.imageTooLarge": "Изображение слишком большое для предварительного просмотра ({{size}}).", + "agentManager.review.imageUnreadable": "Не удалось отобразить это изображение.", + "agentManager.review.imageUnavailable": "Предварительный просмотр изображения недоступен для этого снимка сеанса.", "agentManager.review.endOfLongDiff": "Вы дошли до конца!", "agentManager.import.pullRequest": "Запрос на слияние", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index 26e20065593..270448e6578 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -46,16 +46,17 @@ export const dict = { "agentManager.session.readonly": "เซสชันอ่านอย่างเดียว", "agentManager.session.noSessions": "ไม่มีเซสชันที่เปิดอยู่", "agentManager.tab.close": "ปิด", + "agentManager.tab.closeOthers": "ปิดแท็บอื่น", "agentManager.tab.closeTab": "ปิดแท็บ", "agentManager.tab.forkSession": "แยกเซสชัน", "agentManager.tab.terminal": "เทอร์มินัล", "agentManager.tab.openTerminal": "เปิดเทอร์มินัล", "agentManager.tab.newOptions": "ตัวเลือกแท็บใหม่เพิ่มเติม", - "agentManager.tabsMenu.label": "ค้นหาแท็บที่เปิดอยู่", - "agentManager.tabsMenu.search": "ค้นหาแท็บ...", - "agentManager.tabsMenu.empty": "ไม่มีแท็บที่ตรงกัน", "agentManager.tabsMenu.status.waiting": "รอ", "agentManager.tabsMenu.status.retry": "ลองใหม่", + "agentManager.sidebarSearch.label": "ค้นหา Worktree และเซสชัน", + "agentManager.sidebarSearch.scope": "ค้นหาพื้นที่ทำงานในเครื่อง เซสชันในเครื่อง Worktree และเซสชันของ Worktree", + "agentManager.sidebarSearch.contexts": "ในเครื่อง & WORKTREES", "agentManager.terminal.new": "แท็บเทอร์มินัลใหม่", "agentManager.terminal.ended": "เทอร์มินัลสิ้นสุด — ปิดแท็บเพื่อยกเลิก", @@ -178,6 +179,12 @@ export const dict = { "agentManager.review.collapsedOnly": "ยุบ {{count}} รายการ", "agentManager.review.collapsedWithLarge": "ยุบ {{collapsed}} รายการ, ขนาดใหญ่ {{large}} รายการ", "agentManager.review.largeFileCollapsed": "ไฟล์ขนาดใหญ่ (พับอยู่)", + "agentManager.review.image": "รูปภาพ", + "agentManager.review.imageBefore": "ก่อน", + "agentManager.review.imageAfter": "หลัง", + "agentManager.review.imageTooLarge": "รูปภาพมีขนาดใหญ่เกินกว่าจะแสดงตัวอย่าง ({{size}})", + "agentManager.review.imageUnreadable": "ไม่สามารถแสดงผลรูปภาพนี้ได้", + "agentManager.review.imageUnavailable": "ไม่สามารถแสดงตัวอย่างรูปภาพสำหรับสแนปช็อตของเซสชันนี้ได้", "agentManager.review.endOfLongDiff": "คุณมาถึงท้ายสุดแล้ว!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts index 4ac533f5f4b..2d53ff97de1 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -50,16 +50,18 @@ export const dict = { "agentManager.session.noSessions": "Açık oturum yok", "agentManager.tab.close": "Kapat", + "agentManager.tab.closeOthers": "Diğerlerini kapat", "agentManager.tab.closeTab": "Sekmeyi kapat", "agentManager.tab.forkSession": "Oturumu Fork'la", "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Terminali Aç", "agentManager.tab.newOptions": "Daha fazla yeni sekme seçeneği", - "agentManager.tabsMenu.label": "Açık sekmeleri ara", - "agentManager.tabsMenu.search": "Sekmelerde ara...", - "agentManager.tabsMenu.empty": "Eşleşen sekme yok", "agentManager.tabsMenu.status.waiting": "Bekliyor", "agentManager.tabsMenu.status.retry": "Yeniden", + "agentManager.sidebarSearch.label": "Worktree'leri ve oturumları ara", + "agentManager.sidebarSearch.scope": + "Yerel çalışma alanını, yerel oturumları, Worktree'leri ve bunların oturumlarını arar", + "agentManager.sidebarSearch.contexts": "YEREL & WORKTREE'LER", "agentManager.terminal.new": "Yeni Terminal Sekmesi", "agentManager.terminal.ended": "terminal sona erdi — kapatmak için sekmeyi kapatın", @@ -189,6 +191,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} daraltıldı", "agentManager.review.collapsedWithLarge": "{{collapsed}} daraltıldı, {{large}} büyük", "agentManager.review.largeFileCollapsed": "Büyük dosya (daraltıldı)", + "agentManager.review.image": "Görsel", + "agentManager.review.imageBefore": "Önce", + "agentManager.review.imageAfter": "Sonra", + "agentManager.review.imageTooLarge": "Görsel önizleme için çok büyük ({{size}}).", + "agentManager.review.imageUnreadable": "Bu görsel görüntülenemedi.", + "agentManager.review.imageUnavailable": "Bu oturum anlık görüntüsü için görsel önizlemesi kullanılamıyor.", "agentManager.review.endOfLongDiff": "Sonuna geldiniz!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts index f34fcafe5ce..6b155db49de 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -50,16 +50,18 @@ export const dict = { "agentManager.session.noSessions": "Немає відкритих сесій", "agentManager.tab.close": "Закрити", + "agentManager.tab.closeOthers": "Закрити інші", "agentManager.tab.closeTab": "Закрити вкладку", "agentManager.tab.forkSession": "Розгалужити сесію", "agentManager.tab.terminal": "Термінал", "agentManager.tab.openTerminal": "Відкрити термінал", "agentManager.tab.newOptions": "Інші параметри нової вкладки", - "agentManager.tabsMenu.label": "Шукати відкриті вкладки", - "agentManager.tabsMenu.search": "Шукати вкладки...", - "agentManager.tabsMenu.empty": "Немає відповідних вкладок", "agentManager.tabsMenu.status.waiting": "Очікує", "agentManager.tabsMenu.status.retry": "Повтор", + "agentManager.sidebarSearch.label": "Пошук робочих дерев і сесій", + "agentManager.sidebarSearch.scope": + "Пошук локального робочого простору, локальних сесій, робочих дерев і призначених їм сесій", + "agentManager.sidebarSearch.contexts": "ЛОКАЛЬНИЙ & РОБОЧІ ДЕРЕВА", "agentManager.terminal.new": "Нова вкладка термінала", "agentManager.terminal.ended": "термінал завершено — закрийте вкладку, щоб відхилити", @@ -190,6 +192,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} згорнуто", "agentManager.review.collapsedWithLarge": "{{collapsed}} згорнуто, {{large}} великих", "agentManager.review.largeFileCollapsed": "Великий файл (згорнуто)", + "agentManager.review.image": "Зображення", + "agentManager.review.imageBefore": "До", + "agentManager.review.imageAfter": "Після", + "agentManager.review.imageTooLarge": "Зображення завелике для попереднього перегляду ({{size}}).", + "agentManager.review.imageUnreadable": "Не вдалося відобразити це зображення.", + "agentManager.review.imageUnavailable": "Попередній перегляд зображення недоступний для цього знімка сеансу.", "agentManager.review.endOfLongDiff": "Ви дійшли до кінця!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index fc4460778d5..626b5570d30 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -46,16 +46,17 @@ export const dict = { "agentManager.session.readonly": "只读会话", "agentManager.session.noSessions": "没有打开的会话", "agentManager.tab.close": "关闭", + "agentManager.tab.closeOthers": "关闭其他标签页", "agentManager.tab.closeTab": "关闭标签页", "agentManager.tab.forkSession": "复制会话", "agentManager.tab.terminal": "终端", "agentManager.tab.openTerminal": "打开终端", "agentManager.tab.newOptions": "更多新建标签页选项", - "agentManager.tabsMenu.label": "搜索打开的标签页", - "agentManager.tabsMenu.search": "搜索标签页...", - "agentManager.tabsMenu.empty": "无匹配标签页", "agentManager.tabsMenu.status.waiting": "等待", "agentManager.tabsMenu.status.retry": "重试", + "agentManager.sidebarSearch.label": "搜索 Worktree 和会话", + "agentManager.sidebarSearch.scope": "搜索本地工作区、本地会话、Worktree 及其会话", + "agentManager.sidebarSearch.contexts": "本地 & WORKTREES", "agentManager.terminal.new": "新建终端标签页", "agentManager.terminal.ended": "终端已结束 — 关闭标签页以消除", @@ -177,6 +178,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} 个已折叠", "agentManager.review.collapsedWithLarge": "{{collapsed}} 个已折叠,{{large}} 个过大", "agentManager.review.largeFileCollapsed": "大文件(已折叠)", + "agentManager.review.image": "图片", + "agentManager.review.imageBefore": "之前", + "agentManager.review.imageAfter": "之后", + "agentManager.review.imageTooLarge": "图片过大,无法预览({{size}})。", + "agentManager.review.imageUnreadable": "无法渲染此图片。", + "agentManager.review.imageUnavailable": "此会话快照无法提供图片预览。", "agentManager.review.endOfLongDiff": "你已经到末尾了!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index 96e306580ae..1b1554b09e1 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -46,16 +46,17 @@ export const dict = { "agentManager.session.readonly": "唯讀工作階段", "agentManager.session.noSessions": "沒有開啟的工作階段", "agentManager.tab.close": "關閉", + "agentManager.tab.closeOthers": "關閉其他分頁", "agentManager.tab.closeTab": "關閉分頁", "agentManager.tab.forkSession": "複製工作階段", "agentManager.tab.terminal": "終端機", "agentManager.tab.openTerminal": "開啟終端機", "agentManager.tab.newOptions": "更多新增分頁選項", - "agentManager.tabsMenu.label": "搜尋開啟的分頁", - "agentManager.tabsMenu.search": "搜尋分頁...", - "agentManager.tabsMenu.empty": "沒有相符的分頁", "agentManager.tabsMenu.status.waiting": "等待", "agentManager.tabsMenu.status.retry": "重試", + "agentManager.sidebarSearch.label": "搜尋 Worktree 與工作階段", + "agentManager.sidebarSearch.scope": "搜尋本機工作區、本機工作階段、Worktree 及其工作階段", + "agentManager.sidebarSearch.contexts": "本機 & WORKTREES", "agentManager.terminal.new": "新增終端分頁", "agentManager.terminal.ended": "終端已結束 — 關閉分頁以消除", @@ -177,6 +178,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} 個已摺疊", "agentManager.review.collapsedWithLarge": "{{collapsed}} 個已摺疊,{{large}} 個過大", "agentManager.review.largeFileCollapsed": "大型檔案(已摺疊)", + "agentManager.review.image": "圖片", + "agentManager.review.imageBefore": "變更前", + "agentManager.review.imageAfter": "變更後", + "agentManager.review.imageTooLarge": "圖片太大,無法預覽({{size}})。", + "agentManager.review.imageUnreadable": "無法呈現此圖片。", + "agentManager.review.imageUnavailable": "此工作階段快照無法預覽圖片。", "agentManager.review.endOfLongDiff": "你已經到最後了!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts b/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts index 8f21d698a11..e265a524d14 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts @@ -16,6 +16,7 @@ export function buildShortcutCategories( { title: t("agentManager.shortcuts.category.quickSwitch"), shortcuts: [ + { label: t("agentManager.sidebarSearch.label"), binding: bindings.search ?? "" }, { label: t("agentManager.shortcuts.jumpToItem"), binding: (() => { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/sidebar-search.ts b/packages/kilo-vscode/webview-ui/agent-manager/sidebar-search.ts new file mode 100644 index 00000000000..7be92e0b37e --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/sidebar-search.ts @@ -0,0 +1,225 @@ +import { createMemo } from "solid-js" +import type { Accessor } from "solid-js" +import type { + PermissionRequest, + QuestionRequest, + SectionState, + SessionInfo, + SessionStatusInfo, + WorktreeState, +} from "../src/types/messages" +import { LOCAL } from "./navigate" + +export type SidebarSearchState = "idle" | "busy" | "retry" | "waiting" + +type SearchItem = { + key: string + title: string + meta: string[] + search: string + updatedAt: string + state: SidebarSearchState + visible: boolean + section?: SectionState +} + +export type SidebarSearchItem = + | (SearchItem & { + kind: "local" + group: "contexts" + count: number + }) + | (SearchItem & { + kind: "worktree" + group: "contexts" + worktreeId: string + count: number + }) + | (SearchItem & { + kind: "session" + group: "sessions" + sessionId: string + location: "local" | "worktree" + worktreeId?: string + }) + +export interface SidebarSearchWorktree { + worktree: WorktreeState + label: string + sessions: SessionInfo[] +} + +interface SidebarSearchInput { + worktrees: SidebarSearchWorktree[] + sections: SectionState[] + local: SessionInfo[] + localLabel: string + localBranch?: string + untitled: string + pending: (id: string) => boolean + status: (id: string) => SidebarSearchState + busy: (id: string) => boolean + localBusy: boolean +} + +const root = (item: SessionInfo) => !item.parentID +const same = (a: string, b: string) => a.trim().toLowerCase() === b.trim().toLowerCase() +const score = (state: SidebarSearchState) => (state === "waiting" ? 3 : state === "idle" ? 0 : 2) +const newest = (items: SessionInfo[], fallback: string) => + items.reduce((latest, item) => (item.updatedAt > latest ? item.updatedAt : latest), fallback) + +export function sortSidebarSearch(a: SidebarSearchItem, b: SidebarSearchItem) { + return ( + score(b.state) - score(a.state) || + Number(b.visible) - Number(a.visible) || + b.updatedAt.localeCompare(a.updatedAt) || + a.title.localeCompare(b.title) + ) +} + +export function buildSidebarSearch(input: SidebarSearchInput): SidebarSearchItem[] { + const sections = new Map(input.sections.map((item) => [item.id, item])) + const owned = new Set(input.worktrees.flatMap((item) => item.sessions.map((session) => session.id))) + const local = input.local.filter((session) => root(session) && !input.pending(session.id) && !owned.has(session.id)) + const sessions: SidebarSearchItem[] = local.map((session) => ({ + key: `session:${session.id}`, + kind: "session" as const, + group: "sessions" as const, + title: session.title || input.untitled, + meta: [input.localLabel], + search: [session.title || input.untitled, input.localLabel, session.id].join(" "), + sessionId: session.id, + location: "local" as const, + updatedAt: session.updatedAt, + state: input.status(session.id), + visible: true, + })) + const localState = local.map((session) => input.status(session.id)).sort((a, b) => score(b) - score(a))[0] ?? "idle" + const contexts: SidebarSearchItem[] = [ + { + key: LOCAL, + kind: "local", + group: "contexts", + title: input.localLabel, + meta: input.localBranch ? [input.localBranch] : [], + search: [input.localLabel, input.localBranch].filter(Boolean).join(" "), + updatedAt: newest(local, ""), + state: input.localBusy && localState === "idle" ? "busy" : localState, + visible: true, + count: local.length, + }, + ] + + for (const item of input.worktrees) { + const wt = item.worktree + const section = wt.sectionId ? sections.get(wt.sectionId) : undefined + const roots = item.sessions.filter((session) => root(session) && !input.pending(session.id)) + + for (const session of roots) { + const title = session.title || input.untitled + const meta = [section?.name, !same(item.label, title) ? item.label : undefined, wt.branch].filter( + (value): value is string => !!value, + ) + sessions.push({ + key: `session:${session.id}`, + kind: "session", + group: "sessions", + title, + meta, + search: [title, item.label, wt.branch, section?.name, session.id].filter(Boolean).join(" "), + sessionId: session.id, + location: "worktree", + worktreeId: wt.id, + updatedAt: session.updatedAt, + state: input.status(session.id), + visible: !section?.collapsed, + section, + }) + } + + const state = roots.map((session) => input.status(session.id)).sort((a, b) => score(b) - score(a))[0] ?? "idle" + contexts.push({ + key: `worktree:${wt.id}`, + kind: "worktree", + group: "contexts", + title: item.label, + meta: [section?.name, wt.branch].filter((value): value is string => !!value), + search: [item.label, wt.branch, section?.name, wt.prNumber ? `#${wt.prNumber}` : undefined, wt.id] + .filter(Boolean) + .join(" "), + worktreeId: wt.id, + updatedAt: newest(roots, wt.createdAt), + state: input.busy(wt.id) && state === "idle" ? "busy" : state, + visible: !section?.collapsed, + section, + count: roots.length, + }) + } + + // The List keeps this order for an empty query and as the tie-break order for equally relevant fuzzy matches. + return [...sessions.sort(sortSidebarSearch), ...contexts.sort(sortSidebarSearch)] +} + +interface SidebarSearchDeps { + worktrees: Accessor + sections: Accessor + local: Accessor + localBranch: Accessor + selection: Accessor + sessionId: Accessor + statuses: Accessor> + permissions: Accessor + questions: Accessor + label: (worktree: WorktreeState) => string + sessions: (id: string) => SessionInfo[] + pending: (id: string) => boolean + busy: (id: string) => boolean + localBusy: Accessor + t: (key: string) => string +} + +export function createSidebarSearch(deps: SidebarSearchDeps) { + const items = createMemo(() => { + const statuses = deps.statuses() + const blocked = new Set([ + ...deps.permissions().map((item) => item.sessionID), + ...deps.questions().map((item) => item.sessionID), + ]) + return buildSidebarSearch({ + worktrees: deps.worktrees().map((worktree) => ({ + worktree, + label: deps.label(worktree), + sessions: deps.sessions(worktree.id), + })), + sections: deps.sections(), + local: deps.local(), + localLabel: deps.t("agentManager.local"), + localBranch: deps.localBranch(), + untitled: deps.t("agentManager.session.untitled"), + pending: deps.pending, + status: (id) => { + if (blocked.has(id)) return "waiting" + const status = statuses[id]?.type + return status === "busy" || status === "retry" ? status : "idle" + }, + busy: deps.busy, + localBusy: deps.localBusy(), + }) + }) + + const current = createMemo(() => { + const id = deps.sessionId() + const selection = deps.selection() + const active = items().find( + (item) => + item.kind === "session" && + item.sessionId === id && + ((item.location === "local" && selection === LOCAL) || item.worktreeId === selection), + ) + if (active || !selection) return active + if (selection === LOCAL) return items().find((item) => item.kind === "local") + return items().find((item) => item.kind === "worktree" && item.worktreeId === selection) + }) + + return { items, current } +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/sortable-tab.tsx b/packages/kilo-vscode/webview-ui/agent-manager/sortable-tab.tsx index 6d5a8393377..e47feb4ce03 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/sortable-tab.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/sortable-tab.tsx @@ -52,6 +52,7 @@ export const SortableTab: Component<{ onSelect: () => void onMiddleClick: (e: MouseEvent) => void onClose: () => void + onCloseOthers: () => void onFork?: () => void }> = (props) => { const { t } = useLanguage() @@ -130,6 +131,10 @@ export const SortableTab: Component<{ + + + {t("agentManager.tab.closeOthers")} + diff --git a/packages/kilo-vscode/webview-ui/agent-manager/tab-rendering.tsx b/packages/kilo-vscode/webview-ui/agent-manager/tab-rendering.tsx index 9271a274d7c..7175cccdb5b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/tab-rendering.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/tab-rendering.tsx @@ -8,7 +8,7 @@ */ import { Show } from "solid-js" -import type { JSX } from "solid-js" +import type { Accessor, JSX } from "solid-js" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { DropdownMenu } from "@kilocode/kilo-ui/dropdown-menu" import { Icon } from "@kilocode/kilo-ui/icon" @@ -19,6 +19,45 @@ import { isTerminalTabId, renderTerminalTab } from "./terminal" import type { SessionInfo } from "../src/types/messages" import { parseBindingTokens } from "./keybind-tokens" +interface FocusTabDeps { + id: string + terms: TerminalStateControls + isTerminal: (id: string) => boolean + isPending: (id: string) => boolean + reviewId: string + reviewOpen: Accessor + setReviewOpen: (open: boolean) => void + setReviewActive: (active: boolean) => void + tabLookup: Accessor> + setActivePendingId: (id: string | undefined) => void + clearSession: () => void + selectSession: (id: string) => void + activateTerminal: (id: string) => void +} + +export function focusCurrentTab(deps: FocusTabDeps) { + if (deps.isTerminal(deps.id)) { + deps.activateTerminal(deps.id) + return + } + deps.terms.setActiveId(undefined) + if (deps.id === deps.reviewId) { + if (!deps.reviewOpen()) deps.setReviewOpen(true) + deps.setReviewActive(true) + return + } + const target = deps.tabLookup().get(deps.id) + if (!target) return + deps.setReviewActive(false) + if (deps.isPending(target.id)) { + deps.setActivePendingId(target.id) + deps.clearSession() + return + } + deps.setActivePendingId(undefined) + deps.selectSession(target.id) +} + export interface TabRenderDeps { terms: TerminalStateControls REVIEW_TAB_ID: string @@ -74,6 +113,7 @@ export function renderTab(id: string, deps: TabRenderDeps): JSX.Element { onSelect: deps.activateTerminal, onMiddleClick: deps.terminalMiddleClick, onClose: deps.closeTerminal, + onCloseOthers: (target) => closeOthers(target, deps), }) } if (id === deps.REVIEW_TAB_ID) return renderReviewTab(deps) @@ -141,11 +181,32 @@ function renderSessionTab(s: SessionInfo, deps: TabRenderDeps): JSX.Element { }} onMiddleClick={(e: MouseEvent) => deps.sessionMiddleClick(s.id, e)} onClose={() => deps.sessionClose(s.id)} + onCloseOthers={() => closeOthers(s.id, deps)} onFork={pending ? undefined : () => deps.sessionFork(s.id)} /> ) } +function closeOthers(target: string, deps: TabRenderDeps) { + for (const id of deps.tabIds()) { + if (id === target) continue + if (isTerminalTabId(id)) { + deps.closeTerminal(id) + continue + } + if (id === deps.REVIEW_TAB_ID) { + deps.closeReview() + continue + } + deps.sessionClose(id) + } + if (isTerminalTabId(target)) { + deps.activateTerminal(target) + return + } + deps.selectSessionTab(target, deps.isPending(target)) +} + // Terminal-specific renderers (layer + add button) live in `./terminal/render.tsx` // and are re-exported for convenience so AgentManagerApp.tsx has a single // import point for tab rendering. diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/SortableTerminalTab.tsx b/packages/kilo-vscode/webview-ui/agent-manager/terminal/SortableTerminalTab.tsx index 1c6c9164b89..6357374df9e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/SortableTerminalTab.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/SortableTerminalTab.tsx @@ -33,6 +33,7 @@ export const SortableTerminalTab: Component<{ onSelect: () => void onMiddleClick: (e: MouseEvent) => void onClose: (e: MouseEvent) => void + onCloseOthers: () => void }> = (props) => { const { t } = useLanguage() const sortable = createSortable(props.id) @@ -99,6 +100,10 @@ export const SortableTerminalTab: Component<{ + + + {t("agentManager.tab.closeOthers")} + diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx b/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx index 622fa520705..7b43afc7752 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx @@ -109,7 +109,7 @@ function isAgentManagerShortcut(e: KeyboardEvent): boolean { if (!e.metaKey && !e.ctrlKey) return false const key = e.key.toLowerCase() if (e.altKey && ["arrowleft", "arrowright", "arrowup", "arrowdown"].includes(key)) return true - if (["t", "w", "n", "d", "e"].includes(key)) return true + if (["t", "w", "n", "d", "e", "f"].includes(key)) return true if (e.shiftKey && ["w", "n", "o", "m", "/", "?"].includes(key)) return true if (/^[1-9]$/.test(key)) return true if (key === "/") return true diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/render.tsx b/packages/kilo-vscode/webview-ui/agent-manager/terminal/render.tsx index 2fe5e2c44a0..9c40371e22d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/render.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/render.tsx @@ -22,6 +22,7 @@ export interface TerminalTabRenderDeps { onSelect: (id: string) => void onMiddleClick: (id: string, e: MouseEvent) => void onClose: (id: string) => void + onCloseOthers: (id: string) => void } /** Render the terminal entry inside the agent-manager tab bar ``. */ @@ -43,6 +44,7 @@ export function renderTerminalTab(deps: TerminalTabRenderDeps): JSX.Element { e.stopPropagation() deps.onClose(deps.id) }} + onCloseOthers={() => deps.onCloseOthers(deps.id)} /> ) } diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx index 9a881d61244..b668b74d405 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx @@ -58,6 +58,7 @@ import { import { DiffEndMarker } from "./DiffEndMarker" import { VirtualDiffList } from "./VirtualDiffList" import { isMarkdownFile, MarkdownDiffView } from "./MarkdownDiffView" +import { ImageDiffView } from "./ImageDiffView" import { createDiffRows, diffToken } from "./diff-state" type DiffStyle = "unified" | "split" @@ -96,7 +97,7 @@ export const FullScreenDiffView: Component = (props) => const provider = useProvider() const { config } = useConfig() const speech = useSpeechToText(vscode, server, { t }) - const canUseSpeech = () => canUseSpeechToText(config(), provider.connected(), server.profileData()) + const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) const speechModel = () => selectedSpeechToTextModel(config()) const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) const sendAllKeybind = () => @@ -254,6 +255,15 @@ export const FullScreenDiffView: Component = (props) => ), ) + const request = (diff: WorktreeFileDiff) => { + if (!props.onRequestDiff || props.loadingFiles?.has(diff.file)) return + if (!isDiffExpandable(diff) || diff.summarized !== true) return + const value = diffToken(diff) + if (requested.get(diff.file) === value) return + requested.set(diff.file, value) + props.onRequestDiff(diff.file) + } + createEffect( on( () => [open(), props.diffs] as const, @@ -262,15 +272,10 @@ export const FullScreenDiffView: Component = (props) => for (const file of requested.keys()) { if (!files.has(file)) requested.delete(file) } - if (!props.onRequestDiff) return for (const file of next) { - if (props.loadingFiles?.has(file)) continue const diff = props.diffs.find((item) => item.file === file) - if (!diff || !isDiffExpandable(diff) || diff.summarized !== true) continue - const value = diffToken(diff) - if (requested.get(file) === value) continue - requested.set(file, value) - props.onRequestDiff(file) + if (!diff || diff.kind === "image") continue + request(diff) } }, { defer: true }, @@ -640,6 +645,10 @@ export const FullScreenDiffView: Component = (props) => const isLoadingDetail = () => props.loadingFiles?.has(diff.file) ?? false const fileCommentCount = () => (commentsByFile().get(diff.file) ?? []).length + createEffect(() => { + if (diff.kind === "image" && open().includes(diff.file)) request(diff) + }) + return ( @@ -677,6 +686,9 @@ export const FullScreenDiffView: Component = (props) => + + {t("agentManager.review.image")} + {t("agentManager.review.largeFileCollapsed")} @@ -758,36 +770,43 @@ export const FullScreenDiffView: Component = (props) => } > - before={{ name: diff.file, contents: diff.before }} - after={{ name: diff.file, contents: diff.after }} - patch={diff.patch} - diffStyle={props.diffStyle} - virtualized={shouldVirtualizeDiff(diff)} - annotations={annotationsForFile(diff.file)} - renderAnnotation={buildAnnotation} - enableGutterUtility={props.canComment !== false} - onGutterUtilityClick={(result) => handleGutterClick(diff.file, result)} - onLineNumberClick={(event) => { - if (event.annotationSide === "deletions") return - props.onOpenFile?.(diff.file, event.lineNumber) - }} - /> + + before={{ name: diff.file, contents: diff.before }} + after={{ name: diff.file, contents: diff.after }} + patch={diff.patch} + diffStyle={props.diffStyle} + virtualized={shouldVirtualizeDiff(diff)} + annotations={annotationsForFile(diff.file)} + renderAnnotation={buildAnnotation} + enableGutterUtility={props.canComment !== false} + onGutterUtilityClick={(result) => handleGutterClick(diff.file, result)} + onLineNumberClick={(event) => { + if (event.annotationSide === "deletions") return + props.onOpenFile?.(diff.file, event.lineNumber) + }} + /> + } + > + handleGutterClick(diff.file, result)} + onLineNumberClick={(event) => { + if (event.annotationSide === "deletions") return + props.onOpenFile?.(diff.file, event.lineNumber) + }} + /> + } > - handleGutterClick(diff.file, result)} - onLineNumberClick={(event) => { - if (event.annotationSide === "deletions") return - props.onOpenFile?.(diff.file, event.lineNumber) - }} - /> + diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/ImageDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/ImageDiffView.tsx new file mode 100644 index 00000000000..4d4ae85a2cb --- /dev/null +++ b/packages/kilo-vscode/webview-ui/diff-viewer/ImageDiffView.tsx @@ -0,0 +1,80 @@ +import { type Component, createEffect, createSignal, Show } from "solid-js" +import type { DiffImageSide, WorktreeFileDiff } from "../src/types/messages" +import { useLanguage } from "../src/context/language" + +interface ImageDiffViewProps { + diff: WorktreeFileDiff +} + +interface ImagePaneProps { + file: string + label: string + image: DiffImageSide +} + +function size(bytes: number): string { + if (bytes < 1_000) return `${bytes} B` + if (bytes < 1_000_000) return `${(bytes / 1_000).toFixed(1)} KB` + return `${(bytes / 1_000_000).toFixed(1)} MB` +} + +const ImagePane: Component = (props) => { + const { t } = useLanguage() + const [failed, setFailed] = createSignal(false) + + createEffect(() => { + props.image.data + setFailed(false) + }) + + const message = () => { + if (props.image.error === "too-large") { + return t("agentManager.review.imageTooLarge", { size: size(props.image.bytes) }) + } + if (props.image.error === "unreadable" || failed()) return t("agentManager.review.imageUnreadable") + return undefined + } + + return ( +
+
+ {props.label} + {size(props.image.bytes)} +
+
+ {message()}}> + {`${props.file} setFailed(true)} + /> + +
+
+ ) +} + +export const ImageDiffView: Component = (props) => { + const { t } = useLanguage() + const image = () => props.diff.image + const available = () => Boolean(image()?.before || image()?.after) + + return ( + {t("agentManager.review.imageUnavailable")}
} + > +
+ + {(side) => } + + + {(side) => } + +
+ + ) +} diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/diff-open-policy.ts b/packages/kilo-vscode/webview-ui/diff-viewer/diff-open-policy.ts index c59a22c78db..a0513512057 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/diff-open-policy.ts +++ b/packages/kilo-vscode/webview-ui/diff-viewer/diff-open-policy.ts @@ -14,7 +14,7 @@ export function shouldVirtualizeDiff(diff: WorktreeFileDiff): boolean { } export function isDiffExpandable(diff: WorktreeFileDiff): boolean { - return diff.summarized === true || Boolean(diff.patch || diff.before || diff.after) + return diff.kind === "image" || diff.summarized === true || Boolean(diff.patch || diff.before || diff.after) } export function sanitizeOpenFiles(diffs: WorktreeFileDiff[], open: string[]): string[] { @@ -27,7 +27,7 @@ export function expandableOpenFiles(diffs: WorktreeFileDiff[]): string[] { } export function initialOpenFiles(diffs: WorktreeFileDiff[]): string[] { - return expandableOpenFiles(diffs) + return diffs.filter((diff) => diff.kind !== "image" && isDiffExpandable(diff)).map((diff) => diff.file) } export function allOpenFiles(diffs: WorktreeFileDiff[], open: string[]): boolean { diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/diff-state.ts b/packages/kilo-vscode/webview-ui/diff-viewer/diff-state.ts index 4d977e13ce0..32ebe6db77a 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/diff-state.ts +++ b/packages/kilo-vscode/webview-ui/diff-viewer/diff-state.ts @@ -10,7 +10,8 @@ export function sameDiffMeta(left: WorktreeFileDiff, right: WorktreeFileDiff) { left.tracked === right.tracked && left.generatedLike === right.generatedLike && left.summarized === right.summarized && - left.stamp === right.stamp + left.stamp === right.stamp && + left.kind === right.kind ) } @@ -79,6 +80,7 @@ export function mergeWorktreeDiffs(prev: WorktreeFileDiff[], next: WorktreeFileD existing.before === diff.before && existing.after === diff.after && existing.patch === diff.patch && + existing.image === diff.image && sameDiffMeta(existing, diff) ) return existing @@ -91,12 +93,14 @@ export function mergeWorktreeDiffs(prev: WorktreeFileDiff[], next: WorktreeFileD before: existing.before, after: existing.after, patch: existing.patch, + image: existing.image, summarized: false, } if ( existing.before === merged.before && existing.after === merged.after && existing.patch === merged.patch && + existing.image === merged.image && sameDiffMeta(existing, merged) ) return existing diff --git a/packages/kilo-vscode/webview-ui/kiloclaw/components/ConversationList.tsx b/packages/kilo-vscode/webview-ui/kiloclaw/components/ConversationList.tsx index 8ff31637edb..476d857bdd5 100644 --- a/packages/kilo-vscode/webview-ui/kiloclaw/components/ConversationList.tsx +++ b/packages/kilo-vscode/webview-ui/kiloclaw/components/ConversationList.tsx @@ -5,6 +5,7 @@ import { For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-j import { useClaw } from "../context/claw" import { useKiloClawLanguage } from "../context/language" import type { ConversationListItem } from "../lib/types" +import { isEnterKeyCommitNotIme } from "../../src/utils/ime-enter" type Group = { label: string; items: ConversationListItem[] } @@ -132,7 +133,7 @@ function ConversationItem(props: { conversation: ConversationListItem }) { } const onKey = (e: KeyboardEvent) => { - if (e.key === "Enter") { + if (isEnterKeyCommitNotIme(e)) { e.preventDefault() commitRename() } else if (e.key === "Escape") { diff --git a/packages/kilo-vscode/webview-ui/kiloclaw/components/MessageArea.tsx b/packages/kilo-vscode/webview-ui/kiloclaw/components/MessageArea.tsx index ce9217804b3..d6014362a72 100644 --- a/packages/kilo-vscode/webview-ui/kiloclaw/components/MessageArea.tsx +++ b/packages/kilo-vscode/webview-ui/kiloclaw/components/MessageArea.tsx @@ -10,6 +10,7 @@ import { useKiloClawLanguage } from "../context/language" import { MessageBubble } from "./MessageBubble" import { computeBotDisplay, useNowTicker } from "./botStatus" import type { Message } from "../lib/types" +import { isEnterKeyCommitNotIme } from "../../src/utils/ime-enter" export function MessageArea() { const claw = useClaw() @@ -150,7 +151,7 @@ export function MessageArea() { } const onKeyDown = (e: KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { + if (isEnterKeyCommitNotIme(e) && !e.shiftKey) { e.preventDefault() submit() } diff --git a/packages/kilo-vscode/webview-ui/kiloclaw/components/MessageBubble.tsx b/packages/kilo-vscode/webview-ui/kiloclaw/components/MessageBubble.tsx index a1b8e37c03a..476ec5a9aea 100644 --- a/packages/kilo-vscode/webview-ui/kiloclaw/components/MessageBubble.tsx +++ b/packages/kilo-vscode/webview-ui/kiloclaw/components/MessageBubble.tsx @@ -13,6 +13,7 @@ import { Markdown } from "@kilocode/kilo-ui/markdown" import { showToast } from "@kilocode/kilo-ui/toast" import type { ContentBlock, ExecApprovalDecision, Message } from "../lib/types" import { useKiloClawLanguage } from "../context/language" +import { isEnterKeyCommitNotIme } from "../../src/utils/ime-enter" const ULID_TIME_LEN = 10 const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" @@ -254,7 +255,7 @@ export function MessageBubble(props: MessageBubbleProps) { value={editText()} onInput={(e) => setEditText(e.currentTarget.value)} onKeyDown={(e) => { - if (e.key === "Enter" && !e.shiftKey) { + if (isEnterKeyCommitNotIme(e) && !e.shiftKey) { e.preventDefault() saveEdit() } else if (e.key === "Escape") { diff --git a/packages/kilo-vscode/webview-ui/kiloclaw/components/StatusSidebar.tsx b/packages/kilo-vscode/webview-ui/kiloclaw/components/StatusSidebar.tsx index 1d2246a9d16..b2353b8cd81 100644 --- a/packages/kilo-vscode/webview-ui/kiloclaw/components/StatusSidebar.tsx +++ b/packages/kilo-vscode/webview-ui/kiloclaw/components/StatusSidebar.tsx @@ -6,6 +6,7 @@ import { Show, createMemo, createSignal } from "solid-js" import { useClaw } from "../context/claw" import { useKiloClawLanguage } from "../context/language" +import { isEnterKeyCommitNotIme } from "../../src/utils/ime-enter" function dot(status: string | null | undefined): string { if (!status) return "kiloclaw-dot-offline" @@ -77,7 +78,7 @@ export function StatusSidebar() { } const onTitleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Enter") { + if (isEnterKeyCommitNotIme(e)) { e.preventDefault() commitTitleRename() } else if (e.key === "Escape") { diff --git a/packages/kilo-vscode/webview-ui/pierre-worker.ts b/packages/kilo-vscode/webview-ui/pierre-worker.ts index 83df631b551..46108d1fdc9 100644 --- a/packages/kilo-vscode/webview-ui/pierre-worker.ts +++ b/packages/kilo-vscode/webview-ui/pierre-worker.ts @@ -12,6 +12,7 @@ // the page. Pierre can offload highlighted updates to the pool after its initial // plain render. The diff wrapper still needs to keep that initial render cheap, // which is why review surfaces pass hunk-bounded patches instead of full files. +import { LINE_DIFF_TYPE } from "@kilocode/kilo-ui/pierre" import { WorkerPoolManager } from "@pierre/diffs/worker" import { ensureKiloDiffTheme, KILO_DIFF_THEME } from "@opencode-ai/ui/pierre/kilo-diff-theme" @@ -41,10 +42,10 @@ export function workerFactory(): Worker { return new Worker(url) } -function createPool(lineDiffType: "none" | "word-alt") { +function createPool() { const pool = new WorkerPoolManager( { workerFactory, poolSize: 2 }, - { theme: KILO_DIFF_THEME, lineDiffType, preferredHighlighter: ENGINE }, + { theme: KILO_DIFF_THEME, lineDiffType: LINE_DIFF_TYPE, preferredHighlighter: ENGINE }, ) void pool.initialize().catch((err) => console.warn("[Kilo New] Failed to initialize Pierre worker pool", err)) return pool @@ -59,11 +60,11 @@ export function getWorkerPool(style: WorkerPoolStyle | undefined): WorkerPoolMan if (!uri()) return undefined if (style === "split") { - if (!split) split = createPool("word-alt") + if (!split) split = createPool() return split.isInitialized() ? split : undefined } - if (!unified) unified = createPool("none") + if (!unified) unified = createPool() return unified.isInitialized() ? unified : undefined } diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDiff.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDiff.tsx index d5f8743809d..3136bdf0863 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDiff.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDiff.tsx @@ -76,7 +76,9 @@ export const PermissionDiff: Component = (props) => { when={view()} fallback={
Diff preview unavailable for this file.
} > - {(v) => } + {(v) => ( + + )}
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDock.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDock.tsx index 14534929e10..1ebfa00ccc6 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDock.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDock.tsx @@ -23,6 +23,7 @@ import { PermissionDiff } from "./PermissionDiff" import { permissionDiffs } from "./permission-diff-utils" import { normalizeUrls } from "../../../../../opencode/src/kilocode/util/url" import type { PermissionRequest } from "../../types/messages" +import { isEnterKeyCommitNotIme } from "../../utils/ime-enter" let rulesExpandedPreference = false @@ -132,8 +133,7 @@ export const PermissionDock: Component<{ "button, input, select, textarea, a[href], [contenteditable='true'], [role='button'], [role='menu'], [role='menuitem'], [role='listbox'], [role='option'], [role='combobox'], [role='textbox']", ) - const plain = (e: KeyboardEvent) => - e.key === "Enter" && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey && !e.isComposing + const plain = (e: KeyboardEvent) => isEnterKeyCommitNotIme(e) && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey const skip = (e: KeyboardEvent, target: Element | undefined) => { const local = !!target?.closest("[data-component='permission-shortcuts']") diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index d30f37f1b77..71b87c08ce7 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -48,6 +48,7 @@ import { formatReviewCommentsMarkdown } from "../../utils/review-comment-markdow import { pendingDraftKey, scopeDraftKey, sessionDraftKey } from "../../utils/prompt-drafts" import { ReviewComments } from "./ReviewComments" import { partReview, reviewBody } from "../../../../src/shared/review-comments" +import { isEnterKeyCommitNotIme } from "../../utils/ime-enter" // Per-session input text storage (module-level so it survives remounts) const drafts = new Map() @@ -295,7 +296,7 @@ export const PromptInput: Component = (props) => { const isBusy = () => isPromptBusy(session.status(), !!props.suggesting?.(), !!props.questioning?.(), session.submitting()) const isDisabled = () => !server.isConnected() - const canUseSpeech = () => canUseSpeechToText(config(), provider.connected(), server.profileData()) + const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) const speechModel = () => selectedSpeechToTextModel(config()) const hasInput = () => text().trim().length > 0 || imageAttach.images().length > 0 || reviewComments().length > 0 const canSend = () => @@ -615,7 +616,7 @@ export const PromptInput: Component = (props) => { session.abort() return } - if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { + if (isEnterKeyCommitNotIme(e) && !e.shiftKey) { e.preventDefault() handleSend() } diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx index 0898aebbfe0..06380094d82 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx @@ -19,6 +19,7 @@ import { toggleAnswer, tr, } from "./question-dock-utils" +import { isEnterKeyCommitNotIme } from "../../utils/ime-enter" export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => { const session = useSession() @@ -288,7 +289,7 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => reject() return } - if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { + if (isEnterKeyCommitNotIme(e) && (e.metaKey || e.ctrlKey)) { e.preventDefault() e.stopPropagation() if (store.sending) return diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx index c314e52269f..73dad32da70 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx @@ -336,8 +336,18 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { merged.map((m) => ({ variants: m.variants.map(() => ({})) })), ) setFetchStatus(language.t("provider.custom.models.fetch.added", { count: String(picked.length) })) - setFetchedModels(undefined) - setSearch("") + + // Keep the picker open with the un-picked models so the user can keep adding. + // Only close the picker when every fetched model has been added. + const pickedIds = new Set(picked.map((m) => m.id)) + const remaining = models.filter((m) => !pickedIds.has(m.id)) + if (remaining.length === 0) { + setFetchedModels(undefined) + setSearch("") + } else { + setFetchedModels(remaining) + setSelected(new Set()) + } } function cancelFetch() { diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx index 0b4400918d5..30c240b4f97 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx @@ -1,19 +1,13 @@ import { Component, For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js" import { Switch } from "@kilocode/kilo-ui/switch" -import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { Select } from "@kilocode/kilo-ui/select" import { TextField } from "@kilocode/kilo-ui/text-field" import { Card } from "@kilocode/kilo-ui/card" import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" -import { useProvider } from "../../context/provider" -import { useServer } from "../../context/server" import { useVSCode } from "../../context/vscode" import type { ExtensionMessage } from "../../types/messages" import SettingsRow from "./SettingsRow" -import { DEFAULT_SPEECH_TO_TEXT_MODEL } from "../../../../src/speech-to-text/models" -import { hasSpeechToTextAccess, selectedSpeechToTextModel } from "../speech-to-text/availability" -import { SPEECH_TO_TEXT_MODEL_OPTIONS } from "../speech-to-text/model-selector" interface ShareOption { value: string @@ -29,8 +23,6 @@ const SHARE_OPTIONS: ShareOption[] = [ const ExperimentalTab: Component = () => { const { config, updateConfig } = useConfig() const language = useLanguage() - const provider = useProvider() - const server = useServer() const vscode = useVSCode() const [active, setActive] = createSignal(false) @@ -47,8 +39,6 @@ const ExperimentalTab: Component = () => { }) const experimental = createMemo(() => config().experimental ?? {}) - const kiloReady = createMemo(() => hasSpeechToTextAccess(config(), provider.connected(), server.profileData())) - const speechModel = createMemo(() => selectedSpeechToTextModel(config())) const updateExperimental = (key: string, value: unknown) => { updateConfig({ @@ -164,36 +154,6 @@ const ExperimentalTab: Component = () => { - - - item.value} + label={(item) => `${item.label} (${item.provider})`} + onSelect={(item) => + updateConfig({ + experimental: { + ...config().experimental, + speech_to_text_model: item?.value ?? DEFAULT_SPEECH_TO_TEXT_MODEL.id, + }, + }) + } + variant="secondary" + size="small" + triggerVariant="settings" + triggerProps={{ + "aria-label": `${language.t("settings.models.speechToTextModel.title")}: ${speechOption()?.label}`, + }} + disabled={!kiloReady()} + placeholder={DEFAULT_SPEECH_TO_TEXT_MODEL.label} + /> + + { onCleanup(action.dispose) - const kiloLoggedIn = createMemo(() => !!server.profileData()) + const kiloLoggedIn = createMemo(() => !!provider.authStates()[KILO_PROVIDER_ID]) const connectedProviders = createMemo(() => { const ids = visibleConnectedIds(provider.connected(), provider.authStates()) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx index c693b517ed8..59e8206bff3 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx @@ -145,66 +145,66 @@ const Settings: Component = (props) => { style={{ flex: 1, overflow: "hidden" }} > - + {language.t("settings.models.title")} - + {language.t("settings.providers.title")} - + {language.t("settings.agentBehaviour.title")} - + {language.t("settings.autoApprove.title")} - + {language.t("settings.browser.title")} - + {language.t("settings.checkpoints.title")} - + {language.t("settings.display.title")} - + {language.t("settings.autocomplete.title")} - + {language.t("settings.notifications.title")} - + {language.t("settings.context.title")} - + {language.t("settings.commitMessage.title")} - + {language.t("settings.indexing.title")} - + {language.t("settings.experimental.title")} - + {language.t("settings.language.title")} - + {language.t("settings.aboutKiloCode.title")} diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModeSwitcher.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModeSwitcher.tsx index 4e4524ff5df..6e237dbdda0 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModeSwitcher.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModeSwitcher.tsx @@ -13,6 +13,7 @@ import { Button } from "@kilocode/kilo-ui/button" import { useSession } from "../../context/session" import { useLanguage } from "../../context/language" import type { AgentInfo } from "../../types/messages" +import { isEnterKeyCommitNotIme } from "../../utils/ime-enter" /** Format an agent for display. Uses displayName if available, otherwise title-cases the slug. */ function formatAgentLabel(agent: AgentInfo): string { @@ -87,7 +88,7 @@ export const ModeSwitcherBase: Component = (props) => { } else if (e.key === "End") { e.preventDefault() focusItem(len - 1) - } else if (e.key === "Enter" || e.key === " ") { + } else if (e.key === " " || isEnterKeyCommitNotIme(e)) { e.preventDefault() if (cur >= 0 && cur < len) pick(props.agents[cur].name) } diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx index 00eb157021b..2c53c04948f 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx @@ -31,6 +31,7 @@ import type { EnrichedModel } from "../../context/provider" import { useSession, SessionContext } from "../../context/session" import { useLanguage } from "../../context/language" import type { ModelSelection } from "../../types/messages" +import { isEnterKeyCommitNotIme } from "../../utils/ime-enter" import { KILO_GATEWAY_ID, isSmall, @@ -549,7 +550,7 @@ export const ModelSelectorBase: Component = (props) => { return } - if (e.key === "Enter") { + if (isEnterKeyCommitNotIme(e)) { e.preventDefault() const row = rowMap().get(selectedKey()) if (row) selectRow(row) diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ThinkingSelector.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ThinkingSelector.tsx index 28f1ba4c89c..a6f05f06915 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ThinkingSelector.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ThinkingSelector.tsx @@ -11,6 +11,7 @@ import { type Accessor, Component, createSignal, For, onCleanup, Show } from "so import { PopupSelector } from "./PopupSelector" import { Button } from "@kilocode/kilo-ui/button" import { useSession } from "../../context/session" +import { isEnterKeyCommitNotIme } from "../../utils/ime-enter" // --------------------------------------------------------------------------- // Reusable base component @@ -117,7 +118,7 @@ export const ThinkingSelectorBase: Component = (props focusItem(len - 1) return } - if (e.key === "Enter" || e.key === " ") { + if (e.key === " " || isEnterKeyCommitNotIme(e)) { e.preventDefault() if (cur >= 0 && cur < len) pick(items[cur]) return diff --git a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/availability.ts b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/availability.ts index 32a263c2b6c..3c386670b20 100644 --- a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/availability.ts +++ b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/availability.ts @@ -2,18 +2,23 @@ import { KILO_PROVIDER_ID } from "../../../../src/shared/provider-model" import { getSpeechToTextModel } from "../../../../src/speech-to-text/models" type Cfg = { + enabled_providers?: string[] disabled_providers?: string[] experimental?: { speech_to_text_model?: string } } -export function hasSpeechToTextAccess(cfg: Cfg, providers: readonly string[], profile: unknown | null): boolean { - return providers.includes(KILO_PROVIDER_ID) && !cfg.disabled_providers?.includes(KILO_PROVIDER_ID) && !!profile +type AuthState = "api" | "oauth" | "wellknown" + +export function hasSpeechToTextAccess(cfg: Cfg, auth: Readonly>): boolean { + const enabled = !cfg.enabled_providers || cfg.enabled_providers.includes(KILO_PROVIDER_ID) + const type = auth[KILO_PROVIDER_ID] + return enabled && !cfg.disabled_providers?.includes(KILO_PROVIDER_ID) && (type === "api" || type === "oauth") } -export function canUseSpeechToText(cfg: Cfg, providers: readonly string[], profile: unknown | null): boolean { - return hasSpeechToTextAccess(cfg, providers, profile) +export function canUseSpeechToText(cfg: Cfg, auth: Readonly>): boolean { + return hasSpeechToTextAccess(cfg, auth) } export function selectedSpeechToTextModel(cfg: Cfg): string { diff --git a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/useSpeechToText.ts b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/useSpeechToText.ts index a22afef7f8f..7deb0c06721 100644 --- a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/useSpeechToText.ts +++ b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/useSpeechToText.ts @@ -9,7 +9,6 @@ type VSCode = { } type Server = { - profileData: Accessor goToLogin: () => void } @@ -70,6 +69,10 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe } if (msg.type === "speechToTextError") { + if (msg.code === "not_authenticated") { + login() + return + } fail(msg.error) return } @@ -98,19 +101,6 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe insert = opts.insert setError(undefined) - if (!server.profileData()) { - showToast({ - variant: "error", - title: lang.t("speechToText.error.loginRequired"), - actions: [ - { label: lang.t("common.signIn"), onClick: server.goToLogin }, - { label: lang.t("common.dismiss"), onClick: "dismiss" }, - ], - }) - fail(lang.t("speechToText.error.loginRequired"), false) - return - } - counter++ request = `${prefix}-${counter}` setState("recording") @@ -144,6 +134,19 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe setError(undefined) } + function login() { + const message = lang.t("speechToText.error.loginRequired") + showToast({ + variant: "error", + title: message, + actions: [ + { label: lang.t("common.signIn"), onClick: server.goToLogin }, + { label: lang.t("common.dismiss"), onClick: "dismiss" }, + ], + }) + fail(message, false) + } + function fail(message: string, toast = true) { cleanup() setState("error") diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index cfb54de42a2..01788f5d667 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -368,7 +368,11 @@ export const SessionProvider: ParentComponent = (props) => { const [agents, setAgents] = createSignal([]) const [allAgents, setAllAgents] = createSignal([]) const [defaultAgent, setDefaultAgent] = createSignal("code") - const [pendingKiloModel, setPendingKiloModel] = createSignal<{ modelID: string; after: number } | null>(null) + const [pendingKiloModel, setPendingKiloModel] = createSignal<{ + modelID?: string + agent?: string + after: number + } | null>(null) const [catalog, setCatalog] = createSignal(0) // Skills loaded from the CLI backend @@ -598,9 +602,10 @@ export const SessionProvider: ParentComponent = (props) => { } } - function selectKiloModel(modelID: string) { - setPendingKiloModel({ modelID, after: catalog() }) - vscode.postMessage({ type: "requestProviders" }) + function selectKiloModel(modelID?: string, agent?: string) { + if (!modelID && !agent) return + setPendingKiloModel({ ...(modelID && { modelID }), ...(agent && { agent }), after: catalog() }) + if (modelID) vscode.postMessage({ type: "requestProviders" }) } const unsubKiloModel = vscode.onMessage((message: ExtensionMessage) => { @@ -608,19 +613,24 @@ export const SessionProvider: ParentComponent = (props) => { setCatalog((value) => value + 1) return } - if (message.type === "selectKiloModel") selectKiloModel(message.modelID) + if (message.type === "selectKiloModel") selectKiloModel(message.modelID, message.agent) }) onCleanup(unsubKiloModel) createEffect(() => { const pending = pendingKiloModel() - if (!pending || agents().length === 0 || catalog() <= pending.after) return + if (!pending || agents().length === 0 || (pending.modelID && catalog() <= pending.after)) return setPendingKiloModel(null) - if (!provider.providers()[KILO_PROVIDER_ID]?.models[pending.modelID]) { + if (pending.modelID && !provider.providers()[KILO_PROVIDER_ID]?.models[pending.modelID]) { console.warn("[Kilo New] Ignoring unavailable Kilo catalog model:", pending.modelID) return } - selectModel(KILO_PROVIDER_ID, pending.modelID) + if (pending.agent && !agentNames().has(pending.agent)) { + console.warn("[Kilo New] Ignoring unavailable Kilo agent:", pending.agent) + return + } + if (pending.agent) selectAgent(pending.agent) + if (pending.modelID) selectModel(KILO_PROVIDER_ID, pending.modelID) }) function promptAgent(sessionID?: string) { diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index c4c26ca72f0..cd51e99f228 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1289,10 +1289,10 @@ export const dict = { "settings.experimental.speechToText.title": "تحويل الصوت إلى نص", "settings.experimental.speechToText.description": "تمكين الإدخال الصوتي في حقول المطالبة باستخدام حساب Kilo الخاص بك من خلال Kilo Gateway.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "قم بتمكين وتسجيل الدخول إلى مزود Kilo لاستخدام Speech to Text. ميزة Speech to Text مدعومة حاليًا فقط مع Kilo Gateway.", - "settings.experimental.speechToTextModel.title": "نموذج تحويل الصوت إلى نص", - "settings.experimental.speechToTextModel.description": "اختر نموذج نسخ Kilo Gateway للإدخال الصوتي.", + "settings.models.speechToTextModel.title": "نموذج تحويل الصوت إلى نص", + "settings.models.speechToTextModel.description": "اختر نموذج نسخ Kilo Gateway للإدخال الصوتي.", "settings.experimental.continueOnDeny.title": "المتابعة عند الرفض", "settings.experimental.continueOnDeny.description": "متابعة حلقة الوكيل عند رفض الإذن", "settings.experimental.mcpTimeout.title": "مهلة MCP (مللي ثانية)", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index f898f99b1e9..0a2b9d29ff9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1320,10 +1320,10 @@ export const dict = { "settings.experimental.speechToText.title": "Fala para texto", "settings.experimental.speechToText.description": "Ative a entrada de voz nos campos de prompt usando sua conta do Kilo por meio do Kilo Gateway.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Ative e faça login no provedor Kilo para usar o Speech to Text. Atualmente, o Speech to Text é compatível apenas com o Kilo Gateway.", - "settings.experimental.speechToTextModel.title": "Modelo de fala para texto", - "settings.experimental.speechToTextModel.description": + "settings.models.speechToTextModel.title": "Modelo de fala para texto", + "settings.models.speechToTextModel.description": "Escolha o modelo de transcrição do Kilo Gateway para entrada de voz.", "settings.experimental.continueOnDeny.title": "Continuar ao negar", "settings.experimental.continueOnDeny.description": "Continuar o loop do agente quando uma permissão é negada", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 1c42b81f015..a543b9971f7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1319,11 +1319,10 @@ export const dict = { "settings.experimental.speechToText.title": "Govor u tekst", "settings.experimental.speechToText.description": "Omogućite glasovni unos u poljima za promptove koristeći vaš Kilo račun preko Kilo Gateway.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Omogućite i prijavite se na Kilo provajder da biste koristili Speech to Text. Speech to Text je trenutno podržan samo uz Kilo Gateway.", - "settings.experimental.speechToTextModel.title": "Model govora u tekst", - "settings.experimental.speechToTextModel.description": - "Odaberite Kilo Gateway model za transkripciju za glasovni unos.", + "settings.models.speechToTextModel.title": "Model govora u tekst", + "settings.models.speechToTextModel.description": "Odaberite Kilo Gateway model za transkripciju za glasovni unos.", "settings.experimental.continueOnDeny.title": "Nastavi pri odbijanju", "settings.experimental.continueOnDeny.description": "Nastavi petlju agenta kada je dozvola odbijena", "settings.experimental.mcpTimeout.title": "MCP istek vremena (ms)", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 838ae5f37e0..de4d4bb84ec 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1313,10 +1313,10 @@ export const dict = { "settings.experimental.speechToText.title": "Tale til tekst", "settings.experimental.speechToText.description": "Aktivér stemmeinput i prompt-felter ved hjælp af din Kilo-konto gennem Kilo Gateway.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Aktivér og log ind på Kilo-udbyderen for at bruge Speech to Text. Speech to Text understøttes i øjeblikket kun med Kilo Gateway.", - "settings.experimental.speechToTextModel.title": "Model til tale til tekst", - "settings.experimental.speechToTextModel.description": "Vælg Kilo Gateway-transskriptionsmodellen til stemmeinput.", + "settings.models.speechToTextModel.title": "Model til tale til tekst", + "settings.models.speechToTextModel.description": "Vælg Kilo Gateway-transskriptionsmodellen til stemmeinput.", "settings.experimental.continueOnDeny.title": "Fortsæt ved afvisning", "settings.experimental.continueOnDeny.description": "Fortsæt agentløkken, når en tilladelse afvises", "settings.experimental.mcpTimeout.title": "MCP-timeout (ms)", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 828e7636d8d..f7e00f68fdc 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1336,10 +1336,10 @@ export const dict = { "settings.experimental.speechToText.title": "Sprache zu Text", "settings.experimental.speechToText.description": "Aktivieren Sie die Spracheingabe in Prompt-Feldern mit Ihrem Kilo-Konto über Kilo Gateway.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Aktivieren Sie den Kilo-Anbieter und melden Sie sich an, um Speech to Text zu verwenden. Speech to Text wird derzeit nur mit Kilo Gateway unterstützt.", - "settings.experimental.speechToTextModel.title": "Sprache-zu-Text-Modell", - "settings.experimental.speechToTextModel.description": + "settings.models.speechToTextModel.title": "Sprache-zu-Text-Modell", + "settings.models.speechToTextModel.description": "Wählen Sie das Kilo Gateway-Transkriptionsmodell für die Spracheingabe.", "settings.experimental.continueOnDeny.title": "Bei Ablehnung fortfahren", "settings.experimental.continueOnDeny.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index ec40044432a..d2805027db7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1296,10 +1296,10 @@ export const dict = { "settings.experimental.speechToText.title": "Speech to Text", "settings.experimental.speechToText.description": "Enable voice input in prompt fields using your Kilo account through Kilo Gateway.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Enable and sign in to the Kilo provider to use Speech to Text. Speech to Text is currently only supported through Kilo Gateway.", - "settings.experimental.speechToTextModel.title": "Speech to Text Model", - "settings.experimental.speechToTextModel.description": "Choose the Kilo Gateway transcription model for voice input.", + "settings.models.speechToTextModel.title": "Speech to Text Model", + "settings.models.speechToTextModel.description": "Choose the Kilo Gateway transcription model for voice input.", "settings.experimental.continueOnDeny.title": "Continue on Deny", "settings.experimental.continueOnDeny.description": "Continue the agent loop when a permission is denied", "settings.experimental.mcpTimeout.title": "MCP Timeout (ms)", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index afea5756961..8a00296c400 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1328,10 +1328,10 @@ export const dict = { "settings.experimental.speechToText.title": "Voz a texto", "settings.experimental.speechToText.description": "Habilita la entrada de voz en los campos de prompt usando tu cuenta de Kilo a través de Kilo Gateway.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Habilita e inicia sesión en el proveedor Kilo para usar Speech to Text. Actualmente, Speech to Text solo es compatible con Kilo Gateway.", - "settings.experimental.speechToTextModel.title": "Modelo de voz a texto", - "settings.experimental.speechToTextModel.description": + "settings.models.speechToTextModel.title": "Modelo de voz a texto", + "settings.models.speechToTextModel.description": "Elige el modelo de transcripción de Kilo Gateway para la entrada de voz.", "settings.experimental.continueOnDeny.title": "Continuar al denegar", "settings.experimental.continueOnDeny.description": "Continuar el bucle del agente cuando se deniega un permiso", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index a393d140739..3b2903b2d10 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1343,10 +1343,10 @@ export const dict = { "settings.experimental.speechToText.title": "Transcription vocale", "settings.experimental.speechToText.description": "Activez la saisie vocale dans les champs de prompt en utilisant votre compte Kilo via Kilo Gateway.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Activez et connectez-vous au fournisseur Kilo pour utiliser Speech to Text. Speech to Text n'est actuellement pris en charge qu'avec Kilo Gateway.", - "settings.experimental.speechToTextModel.title": "Modèle de transcription vocale", - "settings.experimental.speechToTextModel.description": + "settings.models.speechToTextModel.title": "Modèle de transcription vocale", + "settings.models.speechToTextModel.description": "Choisissez le modèle de transcription Kilo Gateway pour la saisie vocale.", "settings.experimental.continueOnDeny.title": "Continuer en cas de refus", "settings.experimental.continueOnDeny.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index fda08e0ebd5..3b0e8a41ed5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1523,11 +1523,10 @@ export const dict = { "settings.experimental.speechToText.title": "Da voce a testo", "settings.experimental.speechToText.description": "Abilita input vocale nei campi prompt usando il tuo account Kilo tramite Kilo Gateway.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Abilita e accedi al provider Kilo per usare Da voce a testo. Da voce a testo è attualmente supportato solo tramite Kilo Gateway.", - "settings.experimental.speechToTextModel.title": "Modello Da voce a testo", - "settings.experimental.speechToTextModel.description": - "Scegli il modello di trascrizione Kilo Gateway per l'input vocale.", + "settings.models.speechToTextModel.title": "Modello Da voce a testo", + "settings.models.speechToTextModel.description": "Scegli il modello di trascrizione Kilo Gateway per l'input vocale.", // Compaction limit "settings.context.compactionLimit.title": "Limite compattazione automatica", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index e3d3c1d05db..05373402a49 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1307,10 +1307,10 @@ export const dict = { "settings.experimental.speechToText.title": "音声認識", "settings.experimental.speechToText.description": "Kilo Gateway経由でKiloアカウントを使用して、プロンプトフィールドでの音声入力を有効にします。", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Speech to Text を使用するには、Kilo プロバイダーを有効にしてサインインしてください。現在、Speech to Text は Kilo Gateway でのみサポートされています。", - "settings.experimental.speechToTextModel.title": "音声認識モデル", - "settings.experimental.speechToTextModel.description": "音声入力に使用するKilo Gateway文字起こしモデルを選択します。", + "settings.models.speechToTextModel.title": "音声認識モデル", + "settings.models.speechToTextModel.description": "音声入力に使用するKilo Gateway文字起こしモデルを選択します。", "settings.experimental.continueOnDeny.title": "拒否時に続行", "settings.experimental.continueOnDeny.description": "権限が拒否された場合にエージェントループを続行", "settings.experimental.mcpTimeout.title": "MCPタイムアウト(ミリ秒)", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 06c3f9daea1..e22fe7fd9a8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1301,10 +1301,10 @@ export const dict = { "settings.experimental.speechToText.title": "음성 텍스트 변환", "settings.experimental.speechToText.description": "Kilo Gateway를 통해 Kilo 계정을 사용하여 프롬프트 필드에서 음성 입력을 활성화합니다.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Speech to Text를 사용하려면 Kilo 제공자를 활성화하고 로그인하세요. 현재 Speech to Text는 Kilo Gateway에서만 지원됩니다.", - "settings.experimental.speechToTextModel.title": "음성 텍스트 변환 모델", - "settings.experimental.speechToTextModel.description": "음성 입력에 사용할 Kilo Gateway 변환 모델을 선택하세요.", + "settings.models.speechToTextModel.title": "음성 텍스트 변환 모델", + "settings.models.speechToTextModel.description": "음성 입력에 사용할 Kilo Gateway 변환 모델을 선택하세요.", "settings.experimental.continueOnDeny.title": "거부 시 계속", "settings.experimental.continueOnDeny.description": "권한이 거부되면 에이전트 루프 계속", "settings.experimental.mcpTimeout.title": "MCP 타임아웃 (ms)", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 0215297f7a3..d5a65ac96bf 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1314,10 +1314,10 @@ export const dict = { "settings.experimental.speechToText.title": "Spraak naar tekst", "settings.experimental.speechToText.description": "Schakel spraakinvoer in promptvelden in met uw Kilo-account via Kilo Gateway.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Schakel de Kilo-provider in en meld u aan om Speech to Text te gebruiken. Speech to Text wordt momenteel alleen ondersteund met Kilo Gateway.", - "settings.experimental.speechToTextModel.title": "Spraak-naar-tekst-model", - "settings.experimental.speechToTextModel.description": "Kies het Kilo Gateway-transcriptiemodel voor spraakinvoer.", + "settings.models.speechToTextModel.title": "Spraak-naar-tekst-model", + "settings.models.speechToTextModel.description": "Kies het Kilo Gateway-transcriptiemodel voor spraakinvoer.", "settings.experimental.continueOnDeny.title": "Doorgaan bij weigering", "settings.experimental.continueOnDeny.description": "Ga door met de agent loop wanneer een toestemming wordt geweigerd", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 5581db41f33..9fd3853d353 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1278,10 +1278,10 @@ export const dict = { "settings.experimental.speechToText.title": "Tale til tekst", "settings.experimental.speechToText.description": "Aktiver taleinndata i prompt-felt ved å bruke din Kilo-konto gjennom Kilo Gateway.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Aktiver og logg på Kilo-leverandøren for å bruke Speech to Text. Speech to Text støttes for øyeblikket bare med Kilo Gateway.", - "settings.experimental.speechToTextModel.title": "Tale-til-tekst-modell", - "settings.experimental.speechToTextModel.description": "Velg Kilo Gateway-transkripsjonsmodellen for taleinndata.", + "settings.models.speechToTextModel.title": "Tale-til-tekst-modell", + "settings.models.speechToTextModel.description": "Velg Kilo Gateway-transkripsjonsmodellen for taleinndata.", "settings.experimental.continueOnDeny.title": "Fortsett ved avvisning", "settings.experimental.continueOnDeny.description": "Fortsett agentløkken når en tillatelse avvises", "settings.experimental.mcpTimeout.title": "MCP-tidsavbrudd (ms)", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 74fd42302dc..52f4c4faf59 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1278,10 +1278,10 @@ export const dict = { "settings.experimental.speechToText.title": "Mowa na tekst", "settings.experimental.speechToText.description": "Włącz wprowadzanie głosowe w polach promptów przy użyciu konta Kilo za pośrednictwem Kilo Gateway.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Włącz i zaloguj się do dostawcy Kilo, aby korzystać ze Speech to Text. Speech to Text jest obecnie obsługiwane tylko z Kilo Gateway.", - "settings.experimental.speechToTextModel.title": "Model mowy na tekst", - "settings.experimental.speechToTextModel.description": + "settings.models.speechToTextModel.title": "Model mowy na tekst", + "settings.models.speechToTextModel.description": "Wybierz model transkrypcji Kilo Gateway dla wprowadzania głosowego.", "settings.experimental.continueOnDeny.title": "Kontynuuj przy odmowie", "settings.experimental.continueOnDeny.description": "Kontynuuj pętlę agenta po odmowie uprawnienia", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 97f31a39e7b..9714096ec06 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1318,11 +1318,10 @@ export const dict = { "settings.experimental.speechToText.title": "Речь в текст", "settings.experimental.speechToText.description": "Включите голосовой ввод в полях запросов, используя вашу учетную запись Kilo через Kilo Gateway.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Включите провайдер Kilo и выполните вход, чтобы использовать Speech to Text. В настоящее время Speech to Text поддерживается только с Kilo Gateway.", - "settings.experimental.speechToTextModel.title": "Модель речи в текст", - "settings.experimental.speechToTextModel.description": - "Выберите модель транскрипции Kilo Gateway для голосового ввода.", + "settings.models.speechToTextModel.title": "Модель речи в текст", + "settings.models.speechToTextModel.description": "Выберите модель транскрипции Kilo Gateway для голосового ввода.", "settings.experimental.continueOnDeny.title": "Продолжить при отказе", "settings.experimental.continueOnDeny.description": "Продолжить цикл агента при отказе в разрешении", "settings.experimental.mcpTimeout.title": "Таймаут MCP (мс)", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index da63e9467d5..411f1d672da 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1297,11 +1297,10 @@ export const dict = { "settings.experimental.speechToText.title": "แปลงเสียงเป็นข้อความ", "settings.experimental.speechToText.description": "เปิดใช้งานการป้อนข้อมูลด้วยเสียงในช่องพรอมต์โดยใช้บัญชี Kilo ของคุณผ่าน Kilo Gateway", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "เปิดใช้งานและลงชื่อเข้าใช้ผู้ให้บริการ Kilo เพื่อใช้ Speech to Text ปัจจุบัน Speech to Text รองรับเฉพาะกับ Kilo Gateway เท่านั้น", - "settings.experimental.speechToTextModel.title": "โมเดลแปลงเสียงเป็นข้อความ", - "settings.experimental.speechToTextModel.description": - "เลือกโมเดลการถอดเสียง Kilo Gateway สำหรับการป้อนข้อมูลด้วยเสียง", + "settings.models.speechToTextModel.title": "โมเดลแปลงเสียงเป็นข้อความ", + "settings.models.speechToTextModel.description": "เลือกโมเดลการถอดเสียง Kilo Gateway สำหรับการป้อนข้อมูลด้วยเสียง", "settings.experimental.continueOnDeny.title": "ดำเนินต่อเมื่อถูกปฏิเสธ", "settings.experimental.continueOnDeny.description": "ดำเนินลูปเอเจนต์ต่อเมื่อสิทธิ์ถูกปฏิเสธ", "settings.experimental.mcpTimeout.title": "หมดเวลา MCP (มิลลิวินาที)", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 6b9bcb0da0d..041cc4d8749 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1307,10 +1307,10 @@ export const dict = { "settings.experimental.speechToText.title": "Sesten metne", "settings.experimental.speechToText.description": "Kilo Gateway üzerinden Kilo hesabınızı kullanarak komut alanlarında sesli girişi etkinleştirin.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Speech to Text kullanmak için Kilo sağlayıcısını etkinleştirin ve giriş yapın. Speech to Text şu anda yalnızca Kilo Gateway ile desteklenmektedir.", - "settings.experimental.speechToTextModel.title": "Sesten metne modeli", - "settings.experimental.speechToTextModel.description": "Sesli giriş için Kilo Gateway transkripsiyon modelini seçin.", + "settings.models.speechToTextModel.title": "Sesten metne modeli", + "settings.models.speechToTextModel.description": "Sesli giriş için Kilo Gateway transkripsiyon modelini seçin.", "settings.experimental.continueOnDeny.title": "Reddetme Durumunda Devam Et", "settings.experimental.continueOnDeny.description": "Bir izin reddedildiğinde ajan döngüsüne devam et", "settings.experimental.mcpTimeout.title": "MCP Zaman Aşımı (ms)", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 015da43af9f..2055e0e5af1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1306,11 +1306,10 @@ export const dict = { "settings.experimental.speechToText.title": "Мовлення в текст", "settings.experimental.speechToText.description": "Увімкніть голосове введення в полях запитів, використовуючи ваш обліковий запис Kilo через Kilo Gateway.", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "Увімкніть провайдер Kilo та виконайте вхід, щоб використовувати Speech to Text. Наразі Speech to Text підтримується лише з Kilo Gateway.", - "settings.experimental.speechToTextModel.title": "Модель мовлення в текст", - "settings.experimental.speechToTextModel.description": - "Виберіть модель транскрипції Kilo Gateway для голосового введення.", + "settings.models.speechToTextModel.title": "Модель мовлення в текст", + "settings.models.speechToTextModel.description": "Виберіть модель транскрипції Kilo Gateway для голосового введення.", "settings.experimental.continueOnDeny.title": "Продовжувати при відхиленні", "settings.experimental.continueOnDeny.description": "Продовжувати цикл агента, коли дозвіл відхилено", "settings.experimental.mcpTimeout.title": "Тайм-аут MCP (мс)", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 880fed82d3d..1eda7ef35fb 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1277,10 +1277,10 @@ export const dict = { "settings.experimental.codebaseSearch.description": "启用 AI 驱动的自然语言代码库搜索", "settings.experimental.speechToText.title": "语音转文本", "settings.experimental.speechToText.description": "通过 Kilo Gateway 使用您的 Kilo 帐户在提示词字段中启用语音输入。", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "启用并登录 Kilo 提供商以使用 Speech to Text。Speech to Text 目前仅支持通过 Kilo Gateway 使用。", - "settings.experimental.speechToTextModel.title": "语音转文本模型", - "settings.experimental.speechToTextModel.description": "选择用于语音输入的 Kilo Gateway 转录模型。", + "settings.models.speechToTextModel.title": "语音转文本模型", + "settings.models.speechToTextModel.description": "选择用于语音输入的 Kilo Gateway 转录模型。", "settings.experimental.continueOnDeny.title": "拒绝后继续", "settings.experimental.continueOnDeny.description": "权限被拒绝时继续智能体循环", "settings.experimental.mcpTimeout.title": "MCP 超时(毫秒)", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 8de04ca389b..ca439c70171 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1245,10 +1245,10 @@ export const dict = { "settings.experimental.codebaseSearch.description": "啟用 AI 驅動的自然語言程式碼庫搜尋", "settings.experimental.speechToText.title": "語音轉文字", "settings.experimental.speechToText.description": "透過 Kilo Gateway 使用您的 Kilo 帳戶在提示詞欄位中啟用語音輸入。", - "settings.experimental.speechToText.disabledDescription": + "settings.models.speechToText.disabledDescription": "啟用並登入 Kilo 供應商以使用 Speech to Text。Speech to Text 目前僅支援透過 Kilo Gateway 使用。", - "settings.experimental.speechToTextModel.title": "語音轉文字模型", - "settings.experimental.speechToTextModel.description": "選擇用於語音輸入的 Kilo Gateway 轉錄模型。", + "settings.models.speechToTextModel.title": "語音轉文字模型", + "settings.models.speechToTextModel.description": "選擇用於語音輸入的 Kilo Gateway 轉錄模型。", "settings.experimental.continueOnDeny.title": "拒絕後繼續", "settings.experimental.continueOnDeny.description": "權限被拒絕時繼續 Agent 迴圈", "settings.experimental.mcpTimeout.title": "MCP 逾時(毫秒)", diff --git a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx index 8b387cd3cde..fbad72deba7 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx @@ -15,12 +15,15 @@ import { registerVscodeToolOverrides } from "../components/chat/VscodeToolOverri import { SessionContext } from "../context/session" import { ServerContext } from "../context/server" import { WorktreeModeProvider } from "../context/worktree-mode" +import { SidebarSearchMenu } from "../../agent-manager/SidebarSearchMenu" +import { SidebarToggleButton } from "../../agent-manager/SidebarToggleButton" +import type { SidebarSearchItem } from "../../agent-manager/sidebar-search" import { Button } from "@kilocode/kilo-ui/button" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Icon } from "@kilocode/kilo-ui/icon" import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip" import { ContextMenu } from "@kilocode/kilo-ui/context-menu" -import { createSignal, type JSX } from "solid-js" +import { createSignal, onCleanup, onMount, type JSX } from "solid-js" import type { WorktreeFileDiff, WorktreeState, WorktreeGitStats, PRStatus } from "../types/messages" import type { ReviewComment } from "../../diff-viewer/review-comments" import "../../agent-manager/agent-manager.css" @@ -823,18 +826,9 @@ const MockReviewTab = (props: { active?: boolean }) => (
) -const MockTabsSearchButton = () => ( - -) - const MockTabLeading = () => (
- + {}} />
) @@ -934,3 +928,121 @@ export const TabBarSingleTab: Story = { ), } + +const searchSection = { id: "polish", name: "Polish", color: "Blue", order: 0, collapsed: false } +const slackedSection = { id: "slacked", name: "SLACKED", color: "Yellow", order: 1, collapsed: false } +const sidebarSearchItems: SidebarSearchItem[] = [ + { + key: "session:session-build", + kind: "session", + group: "sessions", + title: "Build grouped worktree search", + meta: ["Polish", "Agent Manager search", "feat/sidebar-search"], + search: "Build grouped worktree search Agent Manager search feat/sidebar-search Polish", + sessionId: "session-build", + location: "worktree", + worktreeId: "wt-search", + updatedAt: new Date(Date.now() - 3 * 60_000).toISOString(), + state: "busy", + visible: true, + section: searchSection, + }, + { + key: "session:session-local", + kind: "session", + group: "sessions", + title: "Investigate local indexing", + meta: ["local"], + search: "Investigate local indexing local", + sessionId: "session-local", + location: "local", + updatedAt: new Date(Date.now() - 8 * 60_000).toISOString(), + state: "idle", + visible: true, + }, + { + key: "session:session-render", + kind: "session", + group: "sessions", + title: "Render images in diff viewer", + meta: ["SLACKED", "images diff viewer", "utopian-approval"], + search: "Render images in diff viewer SLACKED images diff viewer utopian-approval", + sessionId: "session-render", + location: "worktree", + worktreeId: "wt-render", + updatedAt: new Date(Date.now() - 60 * 60_000).toISOString(), + state: "idle", + visible: true, + section: slackedSection, + }, + { + key: "local", + kind: "local", + group: "contexts", + title: "local", + meta: ["main"], + search: "local main", + updatedAt: new Date(Date.now() - 3 * 60_000).toISOString(), + state: "idle", + visible: true, + count: 2, + }, + { + key: "worktree:wt-search", + kind: "worktree", + group: "contexts", + title: "Agent Manager search", + meta: ["Polish", "feat/sidebar-search"], + search: "Agent Manager search Polish feat/sidebar-search", + worktreeId: "wt-search", + updatedAt: new Date(Date.now() - 3 * 60_000).toISOString(), + state: "busy", + visible: true, + section: searchSection, + count: 2, + }, +] + +export const SidebarSearchOpen: Story = { + name: "Sidebar search — worktrees and sessions", + render: () => { + const [selected, setSelected] = createSignal("worktree:wt-search") + let prompt!: HTMLTextAreaElement + const refocus = () => requestAnimationFrame(() => prompt.focus()) + onMount(() => { + window.addEventListener("focusPrompt", refocus) + onCleanup(() => window.removeEventListener("focusPrompt", refocus)) + }) + return ( + +
+
+ +
+ sidebarSearchItems} + keybind="⌘F" + current={() => sidebarSearchItems.find((item) => item.key === selected())} + labels={{ + search: "Search worktrees and sessions", + scope: "Searches the local workspace, local sessions, worktrees, and their sessions", + contexts: "LOCAL & WORKTREES", + sessions: "SESSIONS", + waiting: "Wait", + retry: "Retry", + }} + onSelect={(item) => setSelected(item.key)} + defaultOpen + portal={false} + /> +
+
+ + {selected()} + +