diff --git a/.changeset/compact-thinking-level.md b/.changeset/compact-thinking-level.md new file mode 100644 index 00000000000..a00bd74f635 --- /dev/null +++ b/.changeset/compact-thinking-level.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Preserve the selected thinking level after compacting a session. diff --git a/.changeset/jetbrains-existing-session-scroll.md b/.changeset/jetbrains-existing-session-scroll.md new file mode 100644 index 00000000000..534092cc3f9 --- /dev/null +++ b/.changeset/jetbrains-existing-session-scroll.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Open existing JetBrains sessions scrolled to the latest message after history loads. diff --git a/.changeset/jetbrains-header-tooltips.md b/.changeset/jetbrains-header-tooltips.md new file mode 100644 index 00000000000..eabe4154aa9 --- /dev/null +++ b/.changeset/jetbrains-header-tooltips.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Remember the JetBrains session header metrics expansion preference and show file names in read/write timeline tooltips. diff --git a/.changeset/jetbrains-scroll-bottom.md b/.changeset/jetbrains-scroll-bottom.md new file mode 100644 index 00000000000..b98469fac73 --- /dev/null +++ b/.changeset/jetbrains-scroll-bottom.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Keep the JetBrains chat transcript pinned to bottom reliably while responses stream. diff --git a/.changeset/jetbrains-scroll-button.md b/.changeset/jetbrains-scroll-button.md new file mode 100644 index 00000000000..31762b7e48c --- /dev/null +++ b/.changeset/jetbrains-scroll-button.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show a scroll-to-bottom button when JetBrains chat sessions are scrolled away from the latest message. diff --git a/.changeset/jetbrains-send-action.md b/.changeset/jetbrains-send-action.md new file mode 100644 index 00000000000..6f6f30a3d41 --- /dev/null +++ b/.changeset/jetbrains-send-action.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Support rebinding the JetBrains prompt Send action in the IntelliJ keymap. diff --git a/.changeset/jetbrains-session-header.md b/.changeset/jetbrains-session-header.md new file mode 100644 index 00000000000..9c13aa6c9d5 --- /dev/null +++ b/.changeset/jetbrains-session-header.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show collapsible session metadata, context usage, token usage, and compaction controls in the JetBrains session header. diff --git a/.changeset/jetbrains-session-layout-padding.md b/.changeset/jetbrains-session-layout-padding.md new file mode 100644 index 00000000000..2fbf40c42d8 --- /dev/null +++ b/.changeset/jetbrains-session-layout-padding.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Control JetBrains chat transcript spacing from the session layout. diff --git a/.changeset/jetbrains-session-scroll-layout.md b/.changeset/jetbrains-session-scroll-layout.md new file mode 100644 index 00000000000..ea065c67885 --- /dev/null +++ b/.changeset/jetbrains-session-scroll-layout.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Reduce redundant JetBrains chat scroll layout work while keeping bottom-follow behavior. diff --git a/.changeset/jetbrains-step-finish-timeline.md b/.changeset/jetbrains-step-finish-timeline.md new file mode 100644 index 00000000000..6afba10a4bc --- /dev/null +++ b/.changeset/jetbrains-step-finish-timeline.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show completed assistant steps in the JetBrains session timeline. diff --git a/.changeset/jetbrains-stop-action.md b/.changeset/jetbrains-stop-action.md new file mode 100644 index 00000000000..34bdae579dc --- /dev/null +++ b/.changeset/jetbrains-stop-action.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Support stopping active JetBrains sessions from the prompt action button. diff --git a/.changeset/kiloclaw-kilo-chat-migration.md b/.changeset/kiloclaw-kilo-chat-migration.md new file mode 100644 index 00000000000..59454053337 --- /dev/null +++ b/.changeset/kiloclaw-kilo-chat-migration.md @@ -0,0 +1,8 @@ +--- +"kilo-code": minor +"@kilocode/cli": minor +"@kilocode/kilo-gateway": minor +"@kilocode/sdk": minor +--- + +Migrate KiloClaw chat to the new kilo-chat backend. Replaces the single-channel Stream Chat integration with a multi-conversation experience that matches the web UX at app.kilo.ai/claw/kilo-chat: conversation list, reactions, typing indicators, editing, and action approvals. The TUI continues to render a single chat view backed by the user's primary conversation. diff --git a/.changeset/native-tab-titles.md b/.changeset/native-tab-titles.md new file mode 100644 index 00000000000..2b583182b80 --- /dev/null +++ b/.changeset/native-tab-titles.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Update regular Kilo editor tab titles to follow the current session title. diff --git a/.changeset/remove-custom-providers.md b/.changeset/remove-custom-providers.md new file mode 100644 index 00000000000..b3ffa357735 --- /dev/null +++ b/.changeset/remove-custom-providers.md @@ -0,0 +1,7 @@ +--- +"kilo-code": patch +"@kilocode/cli": patch +"@kilocode/sdk": patch +--- + +Remove custom providers from settings when disconnecting them so they do not reappear after being disabled and re-enabled. diff --git a/.changeset/steady-agents-think.md b/.changeset/steady-agents-think.md new file mode 100644 index 00000000000..c3278a9d444 --- /dev/null +++ b/.changeset/steady-agents-think.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep Agent Manager model, mode, and reasoning effort selections independent per session and preserve them after restart. diff --git a/.changeset/steady-crabs-scroll.md b/.changeset/steady-crabs-scroll.md new file mode 100644 index 00000000000..9688855539f --- /dev/null +++ b/.changeset/steady-crabs-scroll.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Preserve JetBrains chat scroll position when reviewing earlier session output. diff --git a/.changeset/unified-diff-panel.md b/.changeset/unified-diff-panel.md new file mode 100644 index 00000000000..c7c42d5ede4 --- /dev/null +++ b/.changeset/unified-diff-panel.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Unify the Changes panel into a single tab with a source dropdown to switch between workspace and current-session diffs. Show the session's additions and deletions on the sidebar changes badge, and warn in the panel when snapshots are disabled for the repository. diff --git a/.github/actions/setup-git-committer/action.yml b/.github/actions/setup-git-committer/action.yml index 0fff208f62e..0474b670a6e 100644 --- a/.github/actions/setup-git-committer/action.yml +++ b/.github/actions/setup-git-committer/action.yml @@ -39,6 +39,15 @@ runs: - name: Clear checkout auth run: | git config --local --unset-all http.https://github.com/.extraheader || true + # kilocode_change start + keys="$(git config --local --name-only --get-regexp '^includeIf\.gitdir:' || true)" + for key in $keys; do + path="$(git config --local --get "$key" || true)" + case "$path" in + *git-credentials-*.config) git config --local --unset-all "$key" || true ;; + esac + done + # kilocode_change end shell: bash - name: Configure git remote diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 5c5ca6159a6..90fd1021893 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -14,8 +14,9 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change with: + persist-credentials: false # kilocode_change fetch-depth: 0 - name: Setup Bun diff --git a/.github/workflows/check-md-table-padding.yml b/.github/workflows/check-md-table-padding.yml index 51d6706597d..18c3c7aa962 100644 --- a/.github/workflows/check-md-table-padding.yml +++ b/.github/workflows/check-md-table-padding.yml @@ -14,7 +14,7 @@ jobs: if: github.repository == 'Kilo-Org/kilocode' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # kilocode_change with: ref: ${{ github.event.pull_request.head.sha || github.sha }} diff --git a/.github/workflows/check-opencode-annotations.yml b/.github/workflows/check-opencode-annotations.yml index cde9056873d..810371d6d3e 100644 --- a/.github/workflows/check-opencode-annotations.yml +++ b/.github/workflows/check-opencode-annotations.yml @@ -6,9 +6,6 @@ on: - ".github/**" - "github/**" - "sdks/vscode/**" - - "packages/app/**" - - "packages/desktop/**" - - "packages/desktop-electron/**" - "packages/extensions/**" - "packages/opencode/**" - "packages/script/**" @@ -24,7 +21,7 @@ jobs: if: github.repository == 'Kilo-Org/kilocode' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # kilocode_change with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -40,3 +37,8 @@ jobs: else echo "No PR base SHA available (workflow_dispatch without PR context) — skipping." fi + + # kilocode_change start + - name: Check workflow allowlist + run: bun run script/check-workflows.ts + # kilocode_change end diff --git a/.github/workflows/close-issues.yml b/.github/workflows/close-issues.yml index 04b6ae7ac80..aba47e131f1 100644 --- a/.github/workflows/close-issues.yml +++ b/.github/workflows/close-issues.yml @@ -12,7 +12,7 @@ jobs: contents: read issues: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # kilocode_change - uses: oven-sh/setup-bun@v2 with: diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index 03bc1867873..a261d6f5b3b 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -26,7 +26,7 @@ jobs: OWNER: ${{ github.repository_owner }} # kilocode_change end - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # kilocode_change - uses: ./.github/actions/setup-bun diff --git a/.github/workflows/compliance-close.yml b/.github/workflows/disabled/compliance-close.yml.disabled similarity index 96% rename from .github/workflows/compliance-close.yml rename to .github/workflows/disabled/compliance-close.yml.disabled index d56d3b4d971..c3bcf9f686f 100644 --- a/.github/workflows/compliance-close.yml +++ b/.github/workflows/disabled/compliance-close.yml.disabled @@ -13,7 +13,6 @@ permissions: jobs: close-non-compliant: - if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo runs-on: ubuntu-latest steps: - name: Close non-compliant issues and PRs after 2 hours diff --git a/.github/workflows/daily-issues-recap.yml b/.github/workflows/disabled/daily-issues-recap.yml.disabled similarity index 95% rename from .github/workflows/daily-issues-recap.yml rename to .github/workflows/disabled/daily-issues-recap.yml.disabled index 185de2f5b3e..789712ef519 100644 --- a/.github/workflows/daily-issues-recap.yml +++ b/.github/workflows/disabled/daily-issues-recap.yml.disabled @@ -8,9 +8,6 @@ on: jobs: daily-recap: - # kilocode_change start - disabled for kilo-cli fork (OpenCode Discord integration) - if: false - # kilocode_change end runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: read @@ -24,7 +21,7 @@ jobs: - uses: ./.github/actions/setup-bun - name: Install opencode - run: curl -fsSL https://opencode.ai/install | bash + run: curl -fsSL https://kilo.ai/install | bash - name: Generate daily issues recap id: recap @@ -46,7 +43,7 @@ jobs: # Get today's date range TODAY=$(date -u +%Y-%m-%d) - opencode run -m kilo/anthropic/claude-sonnet-4.5 "Generate a daily issues recap for the OpenCode repository. + opencode run -m opencode/claude-sonnet-4-5 "Generate a daily issues recap for the Kilo repository. TODAY'S DATE: ${TODAY} diff --git a/.github/workflows/daily-pr-recap.yml b/.github/workflows/disabled/daily-pr-recap.yml.disabled similarity index 95% rename from .github/workflows/daily-pr-recap.yml rename to .github/workflows/disabled/daily-pr-recap.yml.disabled index 7049531b5dd..ff026917f81 100644 --- a/.github/workflows/daily-pr-recap.yml +++ b/.github/workflows/disabled/daily-pr-recap.yml.disabled @@ -8,9 +8,6 @@ on: jobs: pr-recap: - # kilocode_change start - disabled for kilo-cli fork (OpenCode Discord integration) - if: false - # kilocode_change end runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: read @@ -24,7 +21,7 @@ jobs: - uses: ./.github/actions/setup-bun - name: Install opencode - run: curl -fsSL https://opencode.ai/install | bash + run: curl -fsSL https://kilo.ai/install | bash - name: Generate daily PR recap id: recap @@ -45,7 +42,7 @@ jobs: run: | TODAY=$(date -u +%Y-%m-%d) - opencode run -m kilo/anthropic/claude-sonnet-4.5 "Generate a daily PR activity recap for the OpenCode repository. + opencode run -m opencode/claude-sonnet-4-5 "Generate a daily PR activity recap for the Kilo repository. TODAY'S DATE: ${TODAY} diff --git a/.github/workflows/pr-management.yml b/.github/workflows/disabled/pr-management.yml.disabled similarity index 73% rename from .github/workflows/pr-management.yml rename to .github/workflows/disabled/pr-management.yml.disabled index 7abe0a0ac18..1e331bd6f2e 100644 --- a/.github/workflows/pr-management.yml +++ b/.github/workflows/disabled/pr-management.yml.disabled @@ -5,20 +5,7 @@ on: types: [opened] jobs: - # kilocode_change start - check-author: - if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo - uses: ./.github/workflows/check-org-member.yml - with: - username: ${{ github.event.pull_request.user.login }} - secrets: - kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }} - kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }} - check-duplicates: - needs: check-author - if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo - # kilocode_change end runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: read @@ -48,10 +35,9 @@ jobs: if: steps.team-check.outputs.is_team != 'true' run: bun install - # kilocode_change start - - name: Setup Kilo - uses: ./.github/actions/setup-kilo - # kilocode_change end + - name: Install opencode + if: steps.team-check.outputs.is_team != 'true' + run: curl -fsSL https://kilo.ai/install | bash - name: Build prompt if: steps.team-check.outputs.is_team != 'true' @@ -73,10 +59,7 @@ jobs: - name: Check for duplicate PRs if: steps.team-check.outputs.is_team != 'true' env: - # kilocode_change start - KILO_API_KEY: ${{ secrets.KILO_API_KEY }} - KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }} - # kilocode_change end + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | @@ -89,8 +72,7 @@ jobs: fi add-contributor-label: - if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo - runs-on: blacksmith-2vcpu-ubuntu-2404 # kilocode_change + runs-on: ubuntu-latest permissions: pull-requests: write issues: write diff --git a/.github/workflows/pr-standards.yml b/.github/workflows/disabled/pr-standards.yml.disabled similarity index 90% rename from .github/workflows/pr-standards.yml rename to .github/workflows/disabled/pr-standards.yml.disabled index 75fb650144d..1edbd5d061d 100644 --- a/.github/workflows/pr-standards.yml +++ b/.github/workflows/disabled/pr-standards.yml.disabled @@ -5,22 +5,10 @@ on: types: [opened, edited, synchronize] jobs: - # kilocode_change start - check-author: - if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo - uses: ./.github/workflows/check-org-member.yml - with: - username: ${{ github.event.pull_request.user.login }} - secrets: - kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }} - kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }} - check-standards: - needs: check-author - if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo - # kilocode_change end - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest permissions: + contents: read pull-requests: write steps: - name: Check PR standards @@ -28,6 +16,30 @@ jobs: with: script: | const pr = context.payload.pull_request; + const login = pr.user.login; + + // Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC) + const cutoff = new Date('2026-02-19T00:00:00Z'); + const prCreated = new Date(pr.created_at); + if (prCreated < cutoff) { + console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`); + return; + } + + // Check if author is a team member or bot + if (login === 'opencode-agent[bot]') return; + const { data: file } = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: '.github/TEAM_MEMBERS', + ref: 'dev' + }); + const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean); + if (members.includes(login)) { + console.log(`Skipping: ${login} is a team member`); + return; + } + const title = pr.title; async function addLabel(label) { @@ -59,10 +71,10 @@ jobs: repo: context.repo.repo, issue_number: pr.number }); - + const existing = comments.find(c => c.body.includes(markerText)); if (existing) return; - + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, @@ -88,9 +100,9 @@ jobs: - \`refactor:\` or \`refactor(scope):\` code refactoring - \`test:\` or \`test(scope):\` adding or updating tests - Where \`scope\` is the package name (e.g., \`app\`, \`desktop\`, \`kilo\`). + Where \`scope\` is the package name (e.g., \`app\`, \`desktop\`, \`opencode\`). - See [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md#pr-titles) for details.`); + See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#pr-titles) for details.`); return; } @@ -133,7 +145,7 @@ jobs: 1. Open an issue describing the bug/feature (if one doesn't exist) 2. Add \`Fixes #\` or \`Closes #\` to this PR description - See [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md#issue-first-policy) for details.`); + See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#issue-first-policy) for details.`); return; } @@ -141,7 +153,6 @@ jobs: console.log('PR meets all standards'); check-compliance: - if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/disabled/review.yml.disabled b/.github/workflows/disabled/review.yml.disabled index 542e3a6e4f8..b44d262df7c 100644 --- a/.github/workflows/disabled/review.yml.disabled +++ b/.github/workflows/disabled/review.yml.disabled @@ -49,13 +49,13 @@ jobs: - name: Check PR guidelines compliance env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} KILO_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }' PR_TITLE: ${{ steps.pr-details.outputs.title }} run: | PR_BODY=$(jq -r .body pr_data.json) - opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}' + opencode run -m opencode/gpt-5.5 --variant medium "A new pull request has been created: '${PR_TITLE}' ${{ steps.pr-number.outputs.number }} diff --git a/.github/workflows/storybook.yml b/.github/workflows/disabled/storybook.yml.disabled similarity index 98% rename from .github/workflows/storybook.yml rename to .github/workflows/disabled/storybook.yml.disabled index 9cd606324a1..6d143a8a22f 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/disabled/storybook.yml.disabled @@ -25,7 +25,6 @@ concurrency: jobs: build: - if: false name: storybook build runs-on: blacksmith-4vcpu-ubuntu-2404 steps: diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index b485662e482..316fa748384 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -16,7 +16,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change - name: Setup Bun uses: ./.github/actions/setup-bun diff --git a/.github/workflows/docs-check-links.yml b/.github/workflows/docs-check-links.yml index 75b47a67077..4eb00f444af 100644 --- a/.github/workflows/docs-check-links.yml +++ b/.github/workflows/docs-check-links.yml @@ -17,7 +17,7 @@ jobs: if: github.repository == 'Kilo-Org/kilocode' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # kilocode_change - name: Link Checker uses: Kilo-Org/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml index dfc5039ef24..cf02d8e0a48 100644 --- a/.github/workflows/duplicate-issues.yml +++ b/.github/workflows/duplicate-issues.yml @@ -13,7 +13,7 @@ jobs: issues: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change with: fetch-depth: 1 @@ -128,7 +128,7 @@ jobs: issues: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change with: fetch-depth: 1 diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml index 25b03a7bfc2..2a3f221c176 100644 --- a/.github/workflows/generate.yml +++ b/.github/workflows/generate.yml @@ -14,7 +14,11 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change + # kilocode_change start + with: + persist-credentials: false + # kilocode_change end - name: Setup Bun uses: ./.github/actions/setup-bun diff --git a/.github/workflows/nix-eval.yml b/.github/workflows/nix-eval.yml index dbe5f0bfd05..51c467e1b24 100644 --- a/.github/workflows/nix-eval.yml +++ b/.github/workflows/nix-eval.yml @@ -40,8 +40,6 @@ jobs: SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin" PACKAGES="kilo" - # TODO: move 'desktop' to PACKAGES when #11755 is fixed - OPTIONAL_PACKAGES="desktop" echo "" echo "=== Evaluating packages for all systems ===" @@ -61,23 +59,6 @@ jobs: done done - echo "" - echo "=== Evaluating optional packages ===" - for system in $SYSTEMS; do - echo "" - echo "--- $system ---" - for pkg in $OPTIONAL_PACKAGES; do - printf " %s: " "$pkg" - if output=$(nix eval ".#packages.$system.$pkg.drvPath" --raw 2>&1); then - echo "✓" - else - echo "✗" - echo "::warning::Evaluation failed for packages.$system.$pkg" - echo "$output" - fi - done - done - echo "" echo "=== Evaluating devShells for all systems ===" for system in $SYSTEMS; do diff --git a/.github/workflows/nix-hashes.yml b/.github/workflows/nix-hashes.yml index cfa4ee633a3..be48bb895e1 100644 --- a/.github/workflows/nix-hashes.yml +++ b/.github/workflows/nix-hashes.yml @@ -73,7 +73,7 @@ jobs: echo "Computed hash for ${SYSTEM}: $HASH" - name: Upload hash - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 # kilocode_change with: name: hash-${{ matrix.system }} path: hash.txt @@ -86,7 +86,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change with: persist-credentials: false fetch-depth: 0 @@ -106,7 +106,7 @@ jobs: git pull --rebase --autostash origin "$GITHUB_REF_NAME" - name: Download hash artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 # kilocode_change with: path: hashes pattern: hash-* diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 234048c3641..34f35c22ce0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -42,7 +42,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 if: github.repository == 'Kilo-Org/kilocode' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 # kilocode_change with: fetch-depth: 0 @@ -78,7 +78,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 if: github.repository == 'Kilo-Org/kilocode' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 # kilocode_change with: fetch-tags: true @@ -99,19 +99,20 @@ jobs: # download-artifact@v4's streaming unzip is CPU-bound and processes entries one by one; # collapsing ~480 files into one tar cuts build-vscode download from >30m to seconds. # zstd compresses the wasms (~4x) and leaves the already-compact binaries alone, for - # ~1 GB artifact. compression-level: 0 on the outer zip avoids double-compression. - # zstd + GNU tar are preinstalled on both github-hosted and blacksmith ubuntu-2404. - # See actions/upload-artifact#36, actions/toolkit#1533. + # ~1 GB artifact. upload-artifact@v7's `archive: false` skips the zip wrapper entirely + # so the tarball travels as-is (download-artifact@v8 auto-detects via Content-Type). + # With `archive: false` the `name:` input is ignored; the artifact is named after the + # file (kilo-cli.tar.zst). zstd + GNU tar are preinstalled on both github-hosted and + # blacksmith ubuntu-2404. See actions/upload-artifact#36, actions/toolkit#1533. - name: Pack CLI dist into tar.zst run: | find packages/opencode/dist -name '*.map' -delete tar --zstd -cf /tmp/kilo-cli.tar.zst -C packages/opencode/dist . - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: - name: kilo-cli path: /tmp/kilo-cli.tar.zst - compression-level: 0 + archive: false # kilocode_change end outputs: @@ -122,11 +123,11 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 if: github.repository == 'Kilo-Org/kilocode' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 # kilocode_change - uses: ./.github/actions/setup-bun - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 # kilocode_change with: node-version: "24" registry-url: "https://registry.npmjs.org" @@ -135,10 +136,11 @@ jobs: run: bun install -g @vscode/vsce # kilocode_change start - download into /tmp and extract the tar.zst packed by build-cli - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: - name: kilo-cli + name: kilo-cli.tar.zst path: /tmp + skip-decompress: true - name: Unpack CLI dist run: | @@ -154,127 +156,11 @@ jobs: KILO_PRE_RELEASE: ${{ inputs.pre_release }} GH_REPO: ${{ github.repository }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 # kilocode_change with: name: kilo-vscode path: packages/kilo-vscode/out - # build-tauri: - # needs: - # - build-cli - # - version - # continue-on-error: false - # strategy: - # fail-fast: false - # matrix: - # settings: - # - host: macos-latest - # target: x86_64-apple-darwin - # - host: macos-latest - # target: aarch64-apple-darwin - # - host: blacksmith-4vcpu-windows-2025 - # target: x86_64-pc-windows-msvc - # - host: blacksmith-4vcpu-ubuntu-2404 - # target: x86_64-unknown-linux-gnu - # - host: blacksmith-8vcpu-ubuntu-2404-arm - # target: aarch64-unknown-linux-gnu - # runs-on: ${{ matrix.settings.host }} - # steps: - # - uses: actions/checkout@v3 - # with: - # fetch-tags: true - - # - uses: apple-actions/import-codesign-certs@v2 - # if: ${{ runner.os == 'macOS' }} - # with: - # keychain: build - # p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }} - # p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - - # - name: Verify Certificate - # if: ${{ runner.os == 'macOS' }} - # run: | - # CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application") - # CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}') - # echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV - # echo "Certificate imported." - - # - name: Setup Apple API Key - # if: ${{ runner.os == 'macOS' }} - # run: | - # echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8 - - # - uses: ./.github/actions/setup-bun - - # - name: Cache apt packages - # if: contains(matrix.settings.host, 'ubuntu') - # uses: actions/cache@v4 - # with: - # path: /var/cache/apt/archives - # key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-${{ hashFiles('.github/workflows/publish.yml') }} - # restore-keys: | - # ${{ runner.os }}-${{ matrix.settings.target }}-apt- - - # - name: install dependencies (ubuntu only) - # if: contains(matrix.settings.host, 'ubuntu') - # run: | - # sudo apt-get update - # sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - - # - name: install Rust stable - # uses: dtolnay/rust-toolchain@stable - # with: - # targets: ${{ matrix.settings.target }} - - # - uses: Swatinem/rust-cache@v2 - # with: - # workspaces: packages/desktop/src-tauri - # shared-key: ${{ matrix.settings.target }} - - # - name: Prepare - # run: | - # cd packages/desktop - # bun ./scripts/prepare.ts - # env: - # KILO_VERSION: ${{ needs.version.outputs.version }} - # GITHUB_TOKEN: ${{ steps.committer.outputs.token }} - # RUST_TARGET: ${{ matrix.settings.target }} - # GH_TOKEN: ${{ github.token }} - # GITHUB_RUN_ID: ${{ github.run_id }} - - # # Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released - # - name: Install tauri-cli from portable appimage branch - # if: contains(matrix.settings.host, 'ubuntu') - # run: | - # cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force - # echo "Installed tauri-cli version:" - # cargo tauri --version - - # - name: Build and upload artifacts - # uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a - # timeout-minutes: 60 - # with: - # projectPath: packages/desktop - # uploadWorkflowArtifacts: true - # tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }} - # args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose - # updaterJsonPreferNsis: true - # releaseId: ${{ needs.version.outputs.release }} - # tagName: ${{ needs.version.outputs.tag }} - # releaseDraft: true - # releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext] - # env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true - # TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - # TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - # APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - # APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - # APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }} - # APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} - # APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} - # APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8 - # kilocode_change start # Run smoke tests against CLI assets uploaded to the draft GitHub release # before publishing the release and package artifacts. @@ -295,10 +181,13 @@ jobs: - build-cli - build-vscode - smoke-test - # - build-tauri runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 # kilocode_change + # kilocode_change start + with: + persist-credentials: false + # kilocode_change end - uses: ./.github/actions/setup-bun @@ -315,7 +204,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 # kilocode_change with: node-version: "24" registry-url: "https://registry.npmjs.org" @@ -333,23 +222,24 @@ jobs: # kilocode_change end # kilocode_change start - download into /tmp and extract the tar.zst packed by build-cli - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: - name: kilo-cli + name: kilo-cli.tar.zst path: /tmp + skip-decompress: true - name: Unpack CLI dist run: | mkdir -p packages/opencode/dist tar --zstd -xf /tmp/kilo-cli.tar.zst -C packages/opencode/dist # kilocode_change end - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: kilo-vscode path: packages/kilo-vscode/out - name: Cache apt packages (AUR) - uses: actions/cache@v4 + uses: actions/cache@v5 # kilocode_change with: path: /var/cache/apt/archives key: ${{ runner.os }}-apt-aur-${{ hashFiles('.github/workflows/publish.yml') }} diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index a041bc72569..674235483a3 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -47,7 +47,7 @@ jobs: KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }} steps: - name: Checkout kilo-bench - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change with: repository: Kilo-Org/kilo-bench token: ${{ secrets.BENCH_GITHUB_TOKEN }} @@ -96,6 +96,13 @@ jobs: echo "cli_url=$URL" >> "$GITHUB_OUTPUT" echo "::notice::Testing CLI v${VERSION} via asset API: $URL" + # Harbor's default agent-setup timeout is 360s. The hello-world container + # (FROM ubuntu:24.04) needs apt-get update + apt-get install + a NodeSource + # curl|bash + apt install nodejs before the CLI even downloads, and on + # the Blacksmith runners that occasionally pushes past 6 min when an apt + # mirror or NodeSource cdn is slow, killing the run with + # AgentSetupTimeoutError. 600s gives enough headroom for transient + # mirror/cdn slowness while still finishing well under timeout-minutes. - name: Run smoke test — hello-world env: KILO_CLI_URL: ${{ steps.cli.outputs.cli_url }} @@ -104,7 +111,8 @@ jobs: ./scripts/run_eval.sh \ -m kilo/anthropic/claude-sonnet-4.6 \ -d hello-world \ - --job-name smoke-test-hello-world + --job-name smoke-test-hello-world \ + --agent-setup-timeout 600 - name: Run smoke test — log-summary-date-ranges env: @@ -115,18 +123,26 @@ jobs: -m kilo/anthropic/claude-sonnet-4.6 \ -d terminal-bench-sample \ -t "log-summary-date-ranges" \ - --job-name smoke-test-log-summary + --job-name smoke-test-log-summary \ + --agent-setup-timeout 600 - name: Validate results run: python3 scripts/validate_smoke_test.py jobs/smoke-test-*/ + # Also upload the agent setup logs (stdout/stderr/return-code from the + # CLI install script) so we can see exactly which step stalls when the + # next AgentSetupTimeoutError happens. These come from the install + # script in kilo-bench, which uses `set -euo pipefail` (no `set -x`) + # and never echoes auth tokens or API keys, so the captured output is + # safe to upload. - name: Upload results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 # kilocode_change with: name: smoke-test-results path: | jobs/smoke-test-*/**/result.json jobs/smoke-test-*/**/trajectory.json + jobs/smoke-test-*/**/agent/setup/*.txt retention-days: 30 if-no-files-found: warn diff --git a/.github/workflows/source-check-links.yml b/.github/workflows/source-check-links.yml index 60df85b4b54..659de3d09ba 100644 --- a/.github/workflows/source-check-links.yml +++ b/.github/workflows/source-check-links.yml @@ -16,7 +16,7 @@ jobs: if: github.repository == 'Kilo-Org/kilocode' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # kilocode_change - uses: oven-sh/setup-bun@v2 diff --git a/.github/workflows/test-vscode.yml b/.github/workflows/test-vscode.yml index a3e66fff459..60569585848 100644 --- a/.github/workflows/test-vscode.yml +++ b/.github/workflows/test-vscode.yml @@ -27,7 +27,7 @@ jobs: shell: bash steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2f2dba910de..559d65f0a9f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,21 +28,21 @@ jobs: matrix: settings: - name: linux - host: blacksmith-8vcpu-ubuntu-2404 + host: blacksmith-4vcpu-ubuntu-2404 # kilocode_change - name: windows - host: blacksmith-8vcpu-windows-2025 + host: blacksmith-4vcpu-windows-2025 # kilocode_change runs-on: ${{ matrix.settings.host }} defaults: run: shell: bash steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change with: token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 # kilocode_change with: node-version: "24" @@ -55,7 +55,7 @@ jobs: git config --global user.name "kilo-maintainer[bot]" - name: Cache Turbo - uses: actions/cache@v4 + uses: actions/cache@v5 # kilocode_change with: path: node_modules/.cache/turbo key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-${{ github.sha }} @@ -80,7 +80,7 @@ jobs: - name: Upload unit artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 # kilocode_change with: name: unit-${{ matrix.settings.name }}-${{ github.run_attempt }} include-hidden-files: true @@ -88,87 +88,6 @@ jobs: retention-days: 7 path: packages/*/.artifacts/unit/junit.xml - e2e: - # kilocode_change - disabled: packages/app is not actively maintained - if: false - name: e2e (${{ matrix.settings.name }}) - strategy: - fail-fast: false - matrix: - settings: - - name: linux - host: blacksmith-4vcpu-ubuntu-2404 - - name: windows - host: blacksmith-4vcpu-windows-2025 - runs-on: ${{ matrix.settings.host }} - env: - PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers - defaults: - run: - shell: bash - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: "24" - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Read Playwright version - id: playwright-version - run: | - version=$(node -e 'console.log(require("./package.json").workspaces.catalog["@playwright/test"])') - echo "version=$version" >> "$GITHUB_OUTPUT" - - - name: Cache Playwright browsers - id: playwright-cache - uses: actions/cache@v4 - with: - path: ${{ github.workspace }}/.playwright-browsers - key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright-version.outputs.version }}-chromium - - - name: Install Playwright system dependencies - if: runner.os == 'Linux' - working-directory: packages/app - run: bunx playwright install-deps chromium - - - name: Install Playwright browsers - if: steps.playwright-cache.outputs.cache-hit != 'true' - working-directory: packages/app - run: bunx playwright install chromium - - - name: Run app e2e tests - run: bun --cwd packages/app test:e2e:local - env: - CI: true - # kilocode_change start - KILO_API_KEY: ${{ secrets.KILO_API_KEY }} - KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }} - KILO_DISABLE_SHARE: "true" - KILO_DISABLE_SESSION_INGEST: "true" - KILO_E2E_REQUIRE_PAID: "true" - # kilocode_change end - PLAYWRIGHT_JUNIT_OUTPUT: e2e/junit-${{ matrix.settings.name }}.xml - timeout-minutes: 30 - - - name: Upload Playwright artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: playwright-${{ matrix.settings.name }}-${{ github.run_attempt }} - if-no-files-found: ignore - retention-days: 7 - path: | - packages/app/e2e/junit-*.xml - packages/app/e2e/test-results - packages/app/e2e/playwright-report - required: name: test (linux) runs-on: blacksmith-4vcpu-ubuntu-2404 diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index b767380365e..b467817c71b 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -13,7 +13,7 @@ jobs: issues: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change with: fetch-depth: 1 diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 894f5dac6fe..6c7ae420953 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -12,7 +12,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change - name: Setup Bun uses: ./.github/actions/setup-bun diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml index 4f49c4aa461..d059175adc8 100644 --- a/.github/workflows/visual-regression.yml +++ b/.github/workflows/visual-regression.yml @@ -12,7 +12,7 @@ jobs: matched: ${{ steps.filter.outputs.matched }} is_fork: ${{ steps.fork-check.outputs.is_fork }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # kilocode_change - uses: Kilo-Org/paths-filter@master id: filter with: @@ -47,7 +47,7 @@ jobs: steps: - name: Checkout (internal) if: needs.check-paths.outputs.is_fork != 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change with: lfs: true token: ${{ secrets.BOT_PAT }} @@ -55,7 +55,7 @@ jobs: - name: Checkout (fork) if: needs.check-paths.outputs.is_fork == 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change with: lfs: true @@ -77,7 +77,7 @@ jobs: bun-version: latest - name: Cache Bun modules - uses: actions/cache@v4 + uses: actions/cache@v5 # kilocode_change with: path: ~/.bun/install/cache key: bun-${{ hashFiles('bun.lock') }} @@ -87,7 +87,7 @@ jobs: - name: Cache Playwright browsers id: playwright-cache - uses: actions/cache@v4 + uses: actions/cache@v5 # kilocode_change with: path: ~/.cache/ms-playwright key: playwright-${{ hashFiles('packages/kilo-ui/package.json') }} @@ -103,7 +103,7 @@ jobs: - name: Cache Storybook build id: storybook-cache - uses: actions/cache@v4 + uses: actions/cache@v5 # kilocode_change with: path: packages/kilo-ui/storybook-static key: storybook-${{ hashFiles('packages/kilo-ui/src/**', 'packages/kilo-ui/.storybook/**', 'packages/ui/src/**', 'packages/kilo-ui/package.json') }} @@ -195,7 +195,7 @@ jobs: - name: Upload test results on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 # kilocode_change with: name: visual-regression-results path: packages/kilo-ui/test-results/ @@ -211,7 +211,7 @@ jobs: steps: - name: Checkout (internal) if: needs.check-paths.outputs.is_fork != 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change with: lfs: true token: ${{ secrets.BOT_PAT }} @@ -219,7 +219,7 @@ jobs: - name: Checkout (fork) if: needs.check-paths.outputs.is_fork == 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v6 # kilocode_change with: lfs: true @@ -241,7 +241,7 @@ jobs: bun-version: latest - name: Cache Bun modules - uses: actions/cache@v4 + uses: actions/cache@v5 # kilocode_change with: path: ~/.bun/install/cache key: bun-${{ hashFiles('bun.lock') }} @@ -251,7 +251,7 @@ jobs: - name: Cache Playwright browsers id: playwright-cache-vscode - uses: actions/cache@v4 + uses: actions/cache@v5 # kilocode_change with: path: ~/.cache/ms-playwright key: playwright-vscode-${{ hashFiles('packages/kilo-vscode/package.json') }} @@ -267,7 +267,7 @@ jobs: - name: Cache Storybook build id: storybook-cache-vscode - uses: actions/cache@v4 + uses: actions/cache@v5 # kilocode_change with: path: packages/kilo-vscode/storybook-static key: storybook-vscode-${{ hashFiles('packages/kilo-vscode/webview-ui/src/**', 'packages/kilo-vscode/.storybook/**', 'packages/kilo-ui/src/**', 'packages/ui/src/**', 'packages/kilo-vscode/package.json') }} @@ -386,7 +386,7 @@ jobs: - name: Upload test results on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 # kilocode_change with: name: visual-regression-vscode-results path: packages/kilo-vscode/test-results/ diff --git a/.kilo/command/upstream-manual-merge.md b/.kilo/command/upstream-manual-merge.md new file mode 100644 index 00000000000..cebc4045353 --- /dev/null +++ b/.kilo/command/upstream-manual-merge.md @@ -0,0 +1,263 @@ +--- +description: Resolve upstream merge conflicts +--- + +Resolve the manual part of an upstream merge. + +Arguments: `$ARGUMENTS` + +Use the first argument as the upstream version, for example `v1.1.50` or +`1.1.50`. If no argument is provided, infer the version from the current branch +name, `upstream-merge-report-.md`, or the newest relevant report file. + +## Workflow + +### 1. Inspect the current merge state + +- `git status --short` +- `git diff --name-only --diff-filter=U` +- `upstream-merge-report-.md` when present +- `.worktrees/opencode-merge/auto-merge` for the automated merge snapshot when present + +### 2. Read every conflicted file end-to-end before planning + +Use `script/upstream/find-conflict-markers.sh ` to jump to each region, +then read enough surrounding lines to understand the code — not just the +conflict hunk. Specifically check: + +- is this a plain 3-way on a single expression, or a structural refactor? +- does upstream rename/move something that invalidates a HEAD-only declaration? +- does a `kilocode_change` marker in HEAD encode a bug fix, a feature, or a + defensive check? +- is the conflicted block referenced by *non-conflicted* code elsewhere in the + same file (imports, signatures, call sites) that will break if we drop it? + +When HEAD includes a non-obvious Kilo-specific wrapper (e.g. a helper in +`packages/opencode/src/kilocode/`), find out why it exists before deciding to +keep or bypass it: + +```bash +git log --all --oneline -S "" -- packages/opencode/src/kilocode/ +git log --all --oneline -- +``` + +Look at the commit message and any PR reference. "We wrote our own because of +PR #NNNN" is a real constraint; "we wrote our own because of a typo" is not. + +### 3. Write a plan in chat and get approval + +For every conflicted file (and any adjacent file the resolution forces you to +touch — see §6) include: + +- expected resolution kind: `hybrid`, `take-ours`, `take-theirs`, `regenerated`, + `removed`, `renamed`, or `other` +- risk level: `low`, `medium`, or `high` +- one-sentence rationale (what Kilo behaviour is preserved, what upstream + feature is adopted, what is dropped) +- verification commands you expect to run (targeted tests, typecheck) + +Group files by risk level. Ask the user which batch to start with. You can +resolve an entire `low` batch in one pass if the user approves the batch, but +resolve `medium` and `high` files one at a time. + +**Do not resolve a file until the user has approved that file's (or batch's) +strategy.** + +### 4. Before every edit, explain reasoning before showing the diff + +The user needs to review intent, not just the raw change. For each file, in +order: + +1. Show the conflict's surrounding context (10–30 lines around each conflict + region, in chat). +2. Explain what each of the three sides (HEAD, merge-base, upstream) is doing. +3. State which Kilo behaviour must survive and why (reference PR numbers / + `kilocode_change` comments when possible). +4. State the resolution and why it is better than the alternatives. +5. Then apply the edit. The tool will display the diff — the user only has to + verify the diff matches the reasoning. + +Do not lead with the diff. A diff without reasoning forces the user to +reverse-engineer the decision. + +### 5. Apply resolution rules + +Reference worktrees when present: + +- `.worktrees/opencode-merge/opencode` — pristine upstream tree +- `.worktrees/opencode-merge/kilo-main` — Kilo base snapshot +- `.worktrees/opencode-merge/auto-merge` — automated merge snapshot (original + conflict reference) + +Apply in order: + +- prefer upstream code and architecture whenever compatible with Kilo behaviour +- preserve Kilo-specific behaviour marked with `kilocode_change` +- keep `kilocode_change` markers around Kilo-specific code in shared opencode + files +- when upstream refactors a region that HEAD had annotated with + `kilocode_change`, **check whether the marker encodes a bug fix or a feature + delta**. Bug fixes (missing `await`, defensive null-check, error capture) + usually need to be re-applied on top of the upstream refactor. Example from + v1.14.30: `Workspace.isSyncing` was missing an `await` — upstream's Effect + refactor reintroduced the same bug, so we had to port the fix into the new + `Effect.gen` block. +- when a `take-theirs` drops a line that was the target of a Kilo pre-filter, + the upstream line may be actively wrong for Kilo — e.g. an inner `continue` + filter whose condition collides with an outer filter Kilo added. Re-read the + surrounding 20 lines before committing to `take-theirs`. +- if Kilo-specific code must be refactored to fit new upstream architecture, + explain the refactor in the final summary +- if upstream moved the relevant logic to another file, port the Kilo behaviour + there and list both paths in the final summary. Verify the new file already + carries the Kilo-renamed symbols (e.g. `x-kilo-directory`) by diffing against + pristine upstream. +- if upstream deleted a file, analyse whether the Kilo behaviour should be + ported elsewhere or removed rather than restoring the deleted file +- if tests fail only because upstream intentionally removed behaviour, remove + or update the obsolete tests rather than adding the old file back +- do not modify unrelated files + +When removing code that existed in one side of a conflict, prefer +**commenting it out with `kilocode_change` markers** over deletion when the +surrounding structure (an `if`, a loop) still makes sense. That keeps the +intent visible to the next merger. Example: + +```ts +} else if (input?.scope !== "project" && !Flag.KILO_EXPERIMENTAL_WORKSPACES) { + // kilocode_change start - directory filtering handled by KiloSession.filters above + // if (input?.directory) { + // conditions.push(eq(SessionTable.directory, input.directory)) + // } + // kilocode_change end +} +``` + +Use `TODO:` not `NOTE:` for follow-ups. `TODO` is searchable and implies an +owner will act on it; `NOTE` reads as permanent commentary. + +### 6. Look for adjacent files the conflict forces you to touch + +Upstream restructures sometimes split one file into several (e.g. `permission.ts` +→ `groups/permission.ts` + `handlers/permission.ts`). Only the *renamed* file +shows up in `git diff --diff-filter=U`; the new sibling may need a Kilo feature +ported in too. After resolving the flagged file, check: + +- files that import from the resolved file — do they compile? +- files at paths implied by new imports (e.g. `../middleware/*`, `./handlers/*`) +- `kilocode_change` comments in the *auto-merge* snapshot that didn't end up in + the working tree because the hosting file was renamed + +Add any such files to the plan as `hybrid` or `take-ours` with the same +approval flow. + +### 7. Verify each resolution before moving on + +- confirm `script/upstream/find-conflict-markers.sh ` prints nothing +- read the final file region (the new shape after edit) and sanity-check imports +- for apparently-unused symbols upstream introduced, `grep` the file and the + rest of the package before deleting — they may be called from non-conflicted + code elsewhere. Example: `isTheme` in `theme.tsx` looked unused at the + resolution site but was called twice further down. +- run the smallest relevant check (single `bun test` file, or `bun run + typecheck` in the touched package) +- summarise the exact resolution, tradeoff, and verification result in chat +- ask the user to approve the resolved file before staging it or resolving the + next one (for `medium` / `high`; `low` batches can be staged together) + +### 8. Run the full checks once everything is resolved + +- `git diff --name-only --diff-filter=U` returns empty +- `bun run typecheck` from `packages/opencode/` (targeted) and from repo root + (catches non-conflicted call-site breakage) +- relevant targeted tests. Tests that hang or time out in an unrelated part of + the graph may be pre-existing — note them, don't block the merge on them +- `bun run script/check-opencode-annotations.ts` if `packages/opencode/` shared + files changed. Note that this tool compares against the merge base via `HEAD` + and will be silent until the merge commit lands +- other CI guards that touched files imply (knip for `kilo-vscode/`, + `check-kilocode-change`, source-links, visual regression) + +### 9. Commit with the standard message + +Per `script/upstream/README.md`: + +```bash +git commit -m "resolve merge conflicts" +``` + +The default `git merge` auto-message (`Merge branch '…' into …`) is also fine, +but `resolve merge conflicts` is the convention for these PRs. + +### 10. Resync version strings in a separate commit + +Upstream stamps its own version into shared files — notably +`packages/extensions/zed/extension.toml` (version field + 5 Kilo-Org download +URLs), and any `package.json` that upstream bumped in the same release window. +After the merge this leaves parts of the tree pointing at upstream's version +(e.g. `1.14.30`), whose release tag does not exist on Kilo's pipeline, so the +Zed download URLs silently 404. + +Fix this in a dedicated commit *after* `resolve merge conflicts`: + +```bash +bun run script/sync-versions.ts # uses root package.json version +# or, to target an explicit version: +bun run script/sync-versions.ts 7.2.41 +git add -A +git commit -m "chore: resync versions after upstream merge" +``` + +The script rewrites every top-level `"version"` in `package.json` files +(excluding `node_modules`, hidden dirs, and `packages/kilo-jetbrains/` which +tracks its own cadence), plus the Zed extension toml. It is idempotent — rerun +it any time to rebase the version back onto Kilo main (useful during +long-running upstream merges where `main` releases in the meantime). + +Keeping this in its own commit makes reviewers' job easier: the merge commit +only contains behavioural resolutions, and the version resync is a trivial +diff they can skim in one glance. + +### 11. Write the PR body + +Structure the description so reviewers can skim: + +- **Non-trivial merge decisions**: a short section per file (or group of + related files) that required more than a mechanical `take-ours`/`take-theirs`. + Focus on *what Kilo behaviour survived* and *what upstream features were + adopted*. Link to Kilo PRs when a `kilocode_change` encodes a specific fix. +- **Notable auto-merged changes**: new columns, new helper files, renamed + middleware — anything reviewers should eyeball even though git didn't flag + it. +- **What to test**: explicit, scenario-level test steps for each non-trivial + change. Don't list tests; list *user-visible behaviour* so a tester who + doesn't read the diff can exercise it. +- **CI guards to watch**: typecheck, knip, annotation check, visual regression. +- **Follow-ups**: any `TODO:` you left in code, as a bullet list with links. + +## User-approval checkpoints + +Every manual merge decision requires explicit user approval **before applying** +and **again after verification**. Be especially cautious when a decision is +destructive, changes auth, billing, data deletion, public API compatibility, +config schema behaviour, migrations, provider routing, or security posture. + +## Common pitfalls + +- Auto-merged code can reference declarations that still live inside conflict + blocks. +- Related sibling files can need edits even when they are not listed as + unmerged — especially after upstream structural splits. +- `renamed` should be used only when behaviour moves to a different file. +- Function signatures can drift across conflict boundaries (args added, return + types widened). Grep for every call site before finalising. +- Full-repo typecheck is the catch-all for non-conflicted call-site breakage. +- Upstream can reintroduce bugs a Kilo `kilocode_change` had already fixed — + during big refactors check every `kilocode_change` the refactor touched. +- "Take-theirs" on an inner conditional is often wrong when Kilo added an outer + pre-filter whose whole point was to widen what makes it to the inner block. +- Apparently-unused upstream-added declarations may be called from + non-conflicted code elsewhere. Grep before deleting. +- Stricter DOM lib types (upstream TS upgrade) can surface latent casting + issues around `WebSocket.send`, `Headers`, etc. — prefer narrowing the Kilo + type over adding `any` casts. diff --git a/.kilo/skills/kilocode-merge-minimizer/SKILL.md b/.kilo/skills/kilocode-merge-minimizer/SKILL.md new file mode 100644 index 00000000000..84dfaf4917b --- /dev/null +++ b/.kilo/skills/kilocode-merge-minimizer/SKILL.md @@ -0,0 +1,119 @@ +--- +name: kilocode-merge-minimizer +description: Use when changing shared upstream-owned files, editing or reviewing `kilocode_change` markers, or moving additive Kilo-specific behavior into Kilo-owned code to reduce future merge conflicts. +--- + +# Kilo Merge Minimizer + +Use this skill whenever a normal development task touches shared upstream-owned code and includes Kilo-specific behavior, especially for marker cleanup, extraction work, or `kilocode_change` annotations. + +Do not use this as the primary guide for upstream merge resolution. Upstream merges have their own instructions and should not duplicate that workflow here. + +## Goal + +Minimize Kilo's long-term diff against upstream OpenCode while preserving behavior. + +Prefer this shape for Kilo-specific additions: + +1. Shared upstream file contains only a minimal hook, import, call, registration, or config entry. +2. Kilo-specific behavior lives in Kilo-owned code. +3. Unavoidable shared-file changes have narrow `kilocode_change` markers. +4. The annotation checker passes. + +For changes to existing upstream behavior, prefer the smallest in-place shared-file diff with narrow markers. Do not move changed upstream logic into Kilo-owned code just to avoid textual conflicts, because that can create harder semantic merge conflicts. + +## Core Rules + +- Use `script/check-opencode-annotations.ts` as the source of truth for current shared scopes and exempt paths. +- Use `script/upstream/fix-kilocode-markers.ts` for stale or broad markers, inspecting `--dry-run` output before applying changes. +- Treat upstream-owned files as shared unless the checker or repo ownership rules exempt them. +- Put Kilo-owned UI, CLI, runtime logic, and tests in Kilo-owned paths where practical. +- Avoid adding Kilo business logic directly to shared files. +- Keep shared-file edits as close as possible to upstream shape. +- Do not change shared files unless the change is required for Kilo functionality, fixes a Kilo bug, or is a minimal targeted upstream-quality fix. +- Do not create a large Kilo-only fork for a general upstream-quality improvement. Prefer a minimal targeted fix, or leave the broader change for upstream. +- Do not duplicate upstream logic unless there is a concrete reason. If duplication is unavoidable, isolate the Kilo delta and keep the upstream dependency obvious. + +## Shared File Structure + +- Do not refactor, rename, split files, or extract helpers in shared files just to improve readability or make Kilo extraction cleaner. +- Avoid structural changes that make upstream behavior harder to compare or hide semantic dependency on upstream code. + +## Shared File Style + +- Preserve upstream formatting and import style in shared files, even when it differs from Kilo style. +- Put Kilo-only imports on separate marked lines instead of reorganizing upstream imports. + +## Decision Rules + +Extract Kilo logic when: + +- The change is an additive Kilo feature or integration, not a modification of existing upstream behavior. +- The shared-file change has meaningful Kilo-owned behavior, not just a tiny condition, import, registration, or field. +- The code has loops, branching, error handling, async workflows, storage access, network calls, UI rendering, or telemetry. +- The shared file can become a small orchestrator that calls Kilo helpers. +- The Kilo code is independent enough that extraction will not hide future upstream fixes or behavior changes. + +Keep the change inline when: + +- The Kilo delta is a single field, import, call, simple condition, or small registry entry. +- Extraction would reshape upstream code more than the Kilo change itself. +- The change modifies an upstream algorithm, ordering, heuristic, control flow, or bug fix. +- Extraction would duplicate upstream logic or hide semantic dependency on upstream behavior. +- The Kilo helper closes over upstream-local state. Keep closure-scoped helpers inline and contiguous in one narrow marker block. +- The shared file owns the only route table, enum, schema, switch, or registry where the hook must exist. +- The change restores upstream shape or removes a stale Kilo divergence. + +Always preserve upstream behavior order unless the Kilo behavior change is intentional and tested. + +## Marker Rules + +- Mark only Kilo-specific diff lines in shared upstream files. +- Prefer inline markers for single-line changes: `const value = 42 // kilocode_change`. +- Use block markers only for adjacent Kilo-specific lines: + +```ts +// kilocode_change start +registerKiloFeature(app) +// kilocode_change end +``` + +- Use the file's native comment style, including JSX block comments inside JSX and `#` comments for YAML, TOML, and shell. +- Do not add markers in checker-exempt Kilo-owned paths. +- Remove stale markers when upstream already contains the behavior or when touching Kilo-owned files that still have old markers. +- Use `// kilocode_change - new file` only for unavoidable new Kilo-specific files inside shared upstream paths. + +## Tests + +- Put Kilo-specific CLI/runtime tests in Kilo-owned test paths. +- Move tests out of shared upstream test paths when the behavior under test is Kilo-specific. +- Tests should cover the real failing path, not private or unstable APIs chosen only for convenience. +- Do not add skip gates for required regression coverage. + +## Verification + +After editing shared files or marker comments, run: + +```bash +bun run script/check-opencode-annotations.ts +``` + +If the PR uses a non-default comparison base, pass the correct base ref: + +```bash +bun run script/check-opencode-annotations.ts --base +``` + +For stale or broad markers in one shared file, inspect the dry run before applying: + +```bash +bun run script/upstream/fix-kilocode-markers.ts --dry-run +``` + +Before finishing, confirm: + +- Shared files contain minimal integration points only. +- Kilo logic and tests live in Kilo-owned paths where practical. +- Markers are narrow. +- Stale markers are removed. +- The annotation checker passed, or the reason it could not run is reported. diff --git a/.opencode/skills/effect/SKILL.md b/.opencode/skills/effect/SKILL.md index 47581463774..78216ab01c3 100644 --- a/.opencode/skills/effect/SKILL.md +++ b/.opencode/skills/effect/SKILL.md @@ -1,21 +1,30 @@ --- name: effect -description: Answer questions about the Effect framework +description: Work with Effect v4 / effect-smol TypeScript code in this repo --- # Effect -This codebase uses Effect, a framework for writing typescript. +This codebase uses Effect for typed, composable TypeScript services, schemas, and workflows. -## How to Answer Effect Questions +## Source Of Truth -1. Clone the Effect repository: `https://github.com/Effect-TS/effect-smol` to - `.opencode/references/effect-smol` in this project NOT the skill folder. -2. Use the explore agent to search the codebase for answers about Effect patterns, APIs, and concepts -3. Provide responses based on the actual Effect source code and documentation +Use the current Effect v4 / effect-smol source, not memory or older Effect v2/v3 examples. + +1. If `.opencode/references/effect-smol` is missing, clone `https://github.com/Effect-TS/effect-smol` there. Do this in the project, not in the skill folder. +2. Search `.opencode/references/effect-smol` for exact APIs, examples, tests, and naming patterns before answering or implementing Effect-specific code. +3. Also inspect existing repo code for local house style before introducing new patterns. +4. Prefer answers and implementations backed by specific source files or nearby repo examples. ## Guidelines -- Always use the explore agent with the cloned repository when answering Effect-related questions -- Reference specific files and patterns found in the Effect codebase -- Do not answer from memory - always verify against the source +- Prefer current Effect v4 APIs and project-local patterns over old blog posts, examples, or package-memory guesses. +- Use `Effect.gen(function* () { ... })` for multi-step workflows. +- Use `Effect.fn("Name")` or `Effect.fnUntraced(...)` for named effects when adding reusable service methods or important workflows. +- Prefer Effect `Schema` for API and domain data shapes. Use branded schemas for IDs and `Schema.TaggedErrorClass` for typed domain errors when modeling new error surfaces. +- Keep HTTP handlers thin: decode input, read request context, call services, and map transport errors. Put business rules in services. +- In Effect service code, prefer Effect-aware platform abstractions and dependencies over ad hoc promises where the surrounding code already does so. +- Keep layer composition explicit. Avoid broad hidden provisioning that makes missing dependencies hard to see. +- In tests, prefer the repo's existing Effect test helpers and live tests for filesystem, git, child process, locks, or timing behavior. +- Do not introduce `any`, non-null assertions, unchecked casts, or older Effect APIs just to satisfy types. +- Do not answer from memory. Verify against `.opencode/references/effect-smol` or nearby code first. diff --git a/.opencode/tool/github-triage.ts b/.opencode/tool/github-triage.ts index 2f8635826fa..8f80f868577 100644 --- a/.opencode/tool/github-triage.ts +++ b/.opencode/tool/github-triage.ts @@ -3,8 +3,8 @@ import { tool } from "@kilocode/plugin" const TEAM = { desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"], zen: ["fwang", "MrMushrooooom"], - tui: ["thdxr", "kommander", "rekram1-node"], - core: ["thdxr", "rekram1-node", "jlongster"], + tui: ["kommander", "rekram1-node", "simonklee"], + core: ["kitlangton", "rekram1-node", "jlongster"], docs: ["R44VC0RP"], windows: ["Hona"], } as const diff --git a/AGENTS.md b/AGENTS.md index 1ec324f6802..cc5eda0d054 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,6 +21,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang - **Source links**: After adding or changing URLs in `packages/kilo-vscode/`, `packages/kilo-vscode/webview-ui/`, or `packages/opencode/src/`, run `bun run script/extract-source-links.ts` from the repo root and commit the updated `packages/kilo-docs/source-links.md`. CI runs this check — the build fails if the file is stale. - **kilocode_change check**: `bun run check-kilocode-change` from `packages/kilo-vscode/`. CI runs this — `kilocode_change` is a marker for upstream merge conflicts and must not appear in `packages/kilo-vscode/` or `packages/kilo-ui/` (these are entirely Kilo Code additions). Remove the markers before pushing. - **opencode annotation check**: `bun run script/check-opencode-annotations.ts` from repo root. CI runs this on PRs touching `packages/opencode/` — every Kilo-specific change in shared opencode files must be annotated with `kilocode_change` markers. Exempt paths (no markers needed): `packages/opencode/src/kilocode/`, `packages/opencode/test/kilocode/`, and any path containing `kilocode` in the name. +- **workflow allowlist**: `bun run script/check-workflows.ts` from repo root. CI runs this as part of the annotations workflow — any `.yml` / `.yaml` file added to or removed from `.github/workflows/` must be reflected in the hardcoded list in `script/check-workflows.ts`. Prevents upstream-merged workflows from silently starting to run in our CI. - **Backend/SDK programmatic testing**: see [TESTING.md](./TESTING.md) for spawning the local main-branch backend (`bun dev serve`) and driving it via `curl` — use this instead of `kilo serve` (prod binary) when testing backend fixes. ## Quality Checks @@ -43,10 +44,8 @@ All products are clients of the **CLI** (`packages/opencode/`), which contains t | Product | Package | Description | |---|---|---| -| Kilo CLI | `packages/opencode/` | Core engine. TUI, `kilo run`, `kilo serve`, `kilo web`. Fork of upstream OpenCode. | +| Kilo CLI | `packages/opencode/` | Core engine. TUI, `kilo run`, `kilo serve`. Fork of upstream OpenCode. | | Kilo VS Code Extension | `packages/kilo-vscode/` | VS Code extension. Bundles the CLI binary, spawns `kilo serve` as a child process. Includes the **Agent Manager** — a multi-session orchestration panel with git worktree isolation. | -| OpenCode Desktop | `packages/desktop/` | Standalone Tauri native app. Bundles CLI as sidecar. Single-session UI. Unrelated to the VS Code extension. Not actively maintained — synced from upstream fork. | -| OpenCode Web | `packages/app/` | Shared SolidJS frontend used by both the desktop app and `kilo web` CLI command. Not actively maintained — synced from upstream fork. | **Agent Manager** refers to a feature inside `packages/kilo-vscode/` (extension code in `src/agent-manager/`, webview in `webview-ui/agent-manager/`). It is not a standalone product. See the extension's `AGENTS.md` for details. @@ -69,9 +68,7 @@ Turborepo + Bun workspaces. The packages you'll work with most: | `packages/kilo-gateway/` | `@kilocode/kilo-gateway` | Kilo auth, provider routing, API integration | | `packages/kilo-telemetry/` | `@kilocode/kilo-telemetry` | PostHog analytics + OpenTelemetry | | `packages/kilo-i18n/` | `@kilocode/kilo-i18n` | Internationalization / translations | -| `packages/kilo-ui/` | `@kilocode/kilo-ui` | SolidJS component library shared by the extension webview and `packages/app/` | -| `packages/app/` | `@opencode-ai/app` | Shared SolidJS web UI for desktop app and `kilo web` | -| `packages/desktop/` | `@opencode-ai/desktop` | Tauri desktop app shell | +| `packages/kilo-ui/` | `@kilocode/kilo-ui` | SolidJS component library shared by the extension webview and docs screenshot stories | | `packages/util/` | `@opencode-ai/util` | Shared utilities (error, path, retry, slug, etc.) | | `packages/plugin/` | `@kilocode/plugin` | Plugin/tool interface definitions | @@ -259,6 +256,10 @@ We regularly merge upstream changes from opencode. To minimize merge conflicts a The goal is to keep our diff from upstream as small as possible, making regular merges straightforward and reducing the risk of conflicts. +### Git conflict style + +`bun install` sets `merge.conflictStyle=zdiff3` repo-locally via `script/setup-git.ts` (wired into `postinstall`). Conflicts include the common ancestor between `|||||||` and `=======`, which is what `script/upstream/` and `mergiraf` rely on for structural resolution and what makes manual resolution on shared opencode files tractable. If you've overridden it in your user config, the repo-local setting takes precedence — don't override it back. + ### Kilocode Change Markers To minimize merge conflicts when syncing with upstream, mark Kilo Code-specific changes in shared code with `kilocode_change` comments. diff --git a/TESTING.md b/TESTING.md index 5d17ca66d6d..bee6135c5d2 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,6 +1,6 @@ # TESTING.md -How to spin up the **local main-branch** Kilo backend and test it with `curl` / `fetch`. Aimed at a running Kilo CLI agent iterating on backend fixes without rebuilding the VS Code extension, TUI, or desktop app. +How to spin up the **local main-branch** Kilo backend and test it with `curl` / `fetch`. Aimed at a running Kilo CLI agent iterating on backend fixes without rebuilding the VS Code extension or TUI. All examples use plain shell + `curl`. Writing TypeScript files is a last resort (see Section 8). diff --git a/bun.lock b/bun.lock index a031e862c7c..e7890dfe9b6 100644 --- a/bun.lock +++ b/bun.lock @@ -31,150 +31,44 @@ "turbo": "2.8.13", }, }, - "packages/app": { - "name": "@opencode-ai/app", - "version": "7.2.33", + "packages/core": { + "name": "@opencode-ai/core", + "version": "7.2.42", + "bin": { + "opencode": "./bin/opencode", + }, "dependencies": { - "@kilocode/kilo-i18n": "workspace:*", - "@kilocode/kilo-ui": "workspace:*", - "@kilocode/sdk": "workspace:*", - "@kobalte/core": "catalog:", - "@opencode-ai/shared": "workspace:*", - "@opencode-ai/ui": "workspace:*", - "@shikijs/transformers": "3.9.2", - "@solid-primitives/active-element": "2.1.3", - "@solid-primitives/audio": "1.4.2", - "@solid-primitives/event-bus": "1.1.2", - "@solid-primitives/event-listener": "2.4.5", - "@solid-primitives/i18n": "2.2.1", - "@solid-primitives/media": "2.3.3", - "@solid-primitives/resize-observer": "2.1.5", - "@solid-primitives/scroll": "2.1.3", - "@solid-primitives/storage": "catalog:", - "@solid-primitives/timer": "1.4.4", - "@solid-primitives/websocket": "1.3.1", - "@solidjs/meta": "catalog:", - "@solidjs/router": "catalog:", - "@tanstack/solid-query": "5.91.4", - "@thisbeyond/solid-dnd": "0.7.5", - "diff": "catalog:", + "@effect/opentelemetry": "catalog:", + "@effect/platform-node": "catalog:", + "@npmcli/arborist": "9.4.0", + "@npmcli/config": "10.8.1", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/context-async-hooks": "2.6.1", + "@opentelemetry/exporter-trace-otlp-http": "0.214.0", + "@opentelemetry/sdk-trace-base": "2.6.1", + "cross-spawn": "catalog:", "effect": "catalog:", - "fuzzysort": "catalog:", - "ghostty-web": "github:anomalyco/ghostty-web#main", - "luxon": "catalog:", - "marked": "catalog:", - "marked-shiki": "catalog:", - "remeda": "catalog:", - "shiki": "catalog:", - "solid-js": "catalog:", - "solid-list": "catalog:", - "tailwindcss": "catalog:", - "virtua": "catalog:", + "glob": "13.0.5", + "mime-types": "3.0.2", + "minimatch": "10.2.5", + "npm-package-arg": "13.0.2", + "rotating-file-stream": "3.2.9", + "semver": "^7.6.3", + "xdg-basedir": "5.1.0", "zod": "catalog:", }, "devDependencies": { - "@happy-dom/global-registrator": "20.0.11", - "@playwright/test": "catalog:", - "@tailwindcss/vite": "catalog:", - "@tsconfig/bun": "1.0.9", + "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", - "@types/luxon": "catalog:", - "@types/node": "catalog:", - "@typescript/native-preview": "catalog:", - "typescript": "catalog:", - "vite": "catalog:", - "vite-plugin-icons-spritesheet": "3.0.1", - "vite-plugin-solid": "catalog:", - }, - }, - "packages/desktop": { - "name": "@opencode-ai/desktop", - "version": "7.2.33", - "dependencies": { - "@opencode-ai/app": "workspace:*", - "@opencode-ai/ui": "workspace:*", - "@solid-primitives/i18n": "2.2.1", - "@solid-primitives/storage": "catalog:", - "@solidjs/meta": "catalog:", - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-clipboard-manager": "~2", - "@tauri-apps/plugin-deep-link": "~2", - "@tauri-apps/plugin-dialog": "~2", - "@tauri-apps/plugin-http": "~2", - "@tauri-apps/plugin-notification": "~2", - "@tauri-apps/plugin-opener": "^2", - "@tauri-apps/plugin-os": "~2", - "@tauri-apps/plugin-process": "~2", - "@tauri-apps/plugin-shell": "~2", - "@tauri-apps/plugin-store": "~2", - "@tauri-apps/plugin-updater": "~2", - "@tauri-apps/plugin-window-state": "~2", - "solid-js": "catalog:", - }, - "devDependencies": { - "@actions/artifact": "4.0.0", - "@tauri-apps/cli": "^2", - "@types/bun": "catalog:", - "@typescript/native-preview": "catalog:", - "typescript": "~5.6.2", - "vite": "catalog:", - }, - }, - "packages/desktop-electron": { - "name": "@opencode-ai/desktop-electron", - "version": "7.2.33", - "dependencies": { - "@opencode-ai/app": "workspace:*", - "@opencode-ai/ui": "workspace:*", - "@solid-primitives/i18n": "2.2.1", - "@solid-primitives/storage": "catalog:", - "@solidjs/meta": "catalog:", - "@solidjs/router": "0.15.4", - "drizzle-orm": "catalog:", - "effect": "catalog:", - "electron-context-menu": "4.1.2", - "electron-log": "^5", - "electron-store": "^10", - "electron-updater": "^6", - "electron-window-state": "^5.0.3", - "marked": "^15", - "solid-js": "catalog:", - "tree-kill": "^1.2.2", - }, - "devDependencies": { - "@actions/artifact": "4.0.0", - "@lydell/node-pty": "catalog:", - "@opencode-ai/app": "workspace:*", - "@opencode-ai/ui": "workspace:*", - "@solid-primitives/i18n": "2.2.1", - "@solid-primitives/storage": "catalog:", - "@solidjs/meta": "catalog:", - "@solidjs/router": "0.15.4", - "@types/bun": "catalog:", - "@types/node": "catalog:", - "@typescript/native-preview": "catalog:", - "@valibot/to-json-schema": "1.6.0", - "electron": "41.2.1", - "electron-builder": "^26", - "electron-vite": "^5", - "solid-js": "catalog:", - "sury": "11.0.0-alpha.4", - "typescript": "~5.6.2", - "vite": "catalog:", - "zod-openapi": "5.4.6", - }, - "optionalDependencies": { - "@lydell/node-pty-darwin-arm64": "1.2.0-beta.10", - "@lydell/node-pty-darwin-x64": "1.2.0-beta.10", - "@lydell/node-pty-linux-arm64": "1.2.0-beta.10", - "@lydell/node-pty-linux-x64": "1.2.0-beta.10", - "@lydell/node-pty-win32-arm64": "1.2.0-beta.10", - "@lydell/node-pty-win32-x64": "1.2.0-beta.10", + "@types/cross-spawn": "catalog:", + "@types/npm-package-arg": "6.1.4", + "@types/npmcli__arborist": "6.3.3", + "@types/semver": "catalog:", }, }, "packages/kilo-docs": { "name": "@kilocode/kilo-docs", - "version": "7.2.33", + "version": "7.2.42", "dependencies": { "@docsearch/css": "^4", "@docsearch/js": "^4", @@ -203,7 +97,7 @@ }, "packages/kilo-gateway": { "name": "@kilocode/kilo-gateway", - "version": "7.2.33", + "version": "7.2.42", "dependencies": { "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/anthropic": "3.0.71", @@ -239,7 +133,7 @@ }, "packages/kilo-i18n": { "name": "@kilocode/kilo-i18n", - "version": "7.2.33", + "version": "7.2.42", "devDependencies": { "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", @@ -249,7 +143,7 @@ }, "packages/kilo-indexing": { "name": "@kilocode/kilo-indexing", - "version": "7.2.33", + "version": "7.2.42", "dependencies": { "@aws-sdk/client-bedrock-runtime": "3.1005.0", "@aws-sdk/credential-provider-ini": "3.972.31", @@ -283,7 +177,7 @@ }, "packages/kilo-telemetry": { "name": "@kilocode/kilo-telemetry", - "version": "7.2.33", + "version": "7.2.42", "dependencies": { "@kilocode/kilo-gateway": "workspace:*", "posthog-node": "4.4.0", @@ -297,10 +191,10 @@ }, "packages/kilo-ui": { "name": "@kilocode/kilo-ui", - "version": "7.2.33", + "version": "7.2.42", "dependencies": { "@kobalte/core": "0.13.11", - "@opencode-ai/shared": "workspace:*", + "@opencode-ai/core": "workspace:*", "@pierre/diffs": "catalog:", "@solid-primitives/lifecycle": "0.1.2", "@solid-primitives/media": "2.3.3", @@ -332,7 +226,7 @@ }, "packages/kilo-vscode": { "name": "kilo-code", - "version": "7.2.33", + "version": "7.2.42", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-i18n": "workspace:*", @@ -357,7 +251,6 @@ "quick-lru": "^7.0.0", "simple-git": "3.35.2", "solid-js": "^1.9.11", - "stream-chat": "9.38.0", "uri-js": "^4.4.1", "virtua": "catalog:", "web-tree-sitter": "^0.24.7", @@ -393,7 +286,7 @@ }, "packages/opencode": { "name": "@kilocode/cli", - "version": "7.2.33", + "version": "7.2.42", "bin": { "kilo": "./bin/kilo", "kilocode": "./bin/kilo", @@ -452,8 +345,8 @@ "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "catalog:", + "@opentui/solid": "catalog:", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -464,6 +357,7 @@ "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", @@ -482,7 +376,6 @@ "iconv-lite": "0.7.2", "ignore": "7.0.5", "immer": "11.1.4", - "jschardet": "3.1.4", "jsonc-parser": "3.3.1", "mime-types": "3.0.2", "minimatch": "10.2.5", @@ -490,7 +383,7 @@ "open": "10.1.2", "opencode-gitlab-auth": "2.0.1", "opencode-poe-auth": "0.0.1", - "opentui-spinner": "0.0.6", + "opentui-spinner": "catalog:", "partial-json": "0.1.7", "remeda": "catalog:", "ripgrep": "0.3.1", @@ -498,7 +391,6 @@ "semver": "^7.6.3", "simple-git": "3.35.2", "solid-js": "catalog:", - "stream-chat": "9.38.0", "strip-ansi": "7.1.2", "tree-sitter-bash": "0.25.0", "tree-sitter-powershell": "0.25.10", @@ -518,8 +410,8 @@ "@babel/core": "7.28.4", "@effect/language-service": "0.84.2", "@octokit/webhooks-types": "7.6.1", + "@opencode-ai/core": "workspace:*", "@opencode-ai/script": "workspace:*", - "@opencode-ai/shared": "workspace:*", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", @@ -543,6 +435,7 @@ "@typescript/native-preview": "catalog:", "drizzle-kit": "catalog:", "drizzle-orm": "catalog:", + "prettier": "3.6.2", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", @@ -551,23 +444,23 @@ }, "packages/plugin": { "name": "@kilocode/plugin", - "version": "7.2.33", + "version": "7.2.42", "dependencies": { "@kilocode/sdk": "workspace:*", "effect": "catalog:", "zod": "catalog:", }, "devDependencies": { - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "catalog:", + "@opentui/solid": "catalog:", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "typescript": "catalog:", }, "peerDependencies": { - "@opentui/core": ">=0.1.100", - "@opentui/solid": ">=0.1.100", + "@opentui/core": ">=0.1.105", + "@opentui/solid": ">=0.1.105", }, "optionalPeers": [ "@opentui/core", @@ -576,7 +469,7 @@ }, "packages/script": { "name": "@opencode-ai/script", - "version": "7.2.33", + "version": "7.2.42", "dependencies": { "semver": "^7.6.3", }, @@ -587,7 +480,7 @@ }, "packages/sdk/js": { "name": "@kilocode/sdk", - "version": "7.2.33", + "version": "7.2.42", "dependencies": { "cross-spawn": "catalog:", }, @@ -600,33 +493,9 @@ "typescript": "catalog:", }, }, - "packages/shared": { - "name": "@opencode-ai/shared", - "version": "7.2.33", - "bin": { - "opencode": "./bin/opencode", - }, - "dependencies": { - "@effect/platform-node": "catalog:", - "@npmcli/arborist": "catalog:", - "effect": "catalog:", - "glob": "13.0.5", - "mime-types": "3.0.2", - "minimatch": "10.2.5", - "semver": "catalog:", - "xdg-basedir": "5.1.0", - "zod": "catalog:", - }, - "devDependencies": { - "@tsconfig/bun": "catalog:", - "@types/bun": "catalog:", - "@types/npmcli__arborist": "6.3.3", - "@types/semver": "catalog:", - }, - }, "packages/storybook": { "name": "@opencode-ai/storybook", - "version": "7.2.33", + "version": "7.2.42", "devDependencies": { "@opencode-ai/ui": "workspace:*", "@solidjs/meta": "catalog:", @@ -649,11 +518,11 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "7.2.33", + "version": "7.2.42", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "catalog:", - "@opencode-ai/shared": "workspace:*", + "@opencode-ai/core": "workspace:*", "@pierre/diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/bounds": "0.1.3", @@ -671,6 +540,7 @@ "marked": "catalog:", "marked-katex-extension": "5.1.6", "marked-shiki": "catalog:", + "mermaid": "11.14.0", "morphdom": "2.7.8", "motion": "12.34.5", "motion-dom": "12.34.3", @@ -702,14 +572,12 @@ "esbuild", "tree-sitter-powershell", "protobufjs", - "electron", "web-tree-sitter", "tree-sitter-bash", ], "patchedDependencies": { "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch", - "stream-chat@9.38.0": "patches/stream-chat@9.38.0.patch", }, "overrides": { "@effect/platform-node-shared": "4.0.0-beta.46", @@ -729,16 +597,16 @@ }, "catalog": { "@cloudflare/workers-types": "4.20251008.0", - "@effect/opentelemetry": "4.0.0-beta.48", - "@effect/platform-node": "4.0.0-beta.48", + "@effect/opentelemetry": "4.0.0-beta.57", + "@effect/platform-node": "4.0.0-beta.57", "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", "@lydell/node-pty": "1.2.0-beta.10", "@npmcli/arborist": "9.4.0", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "0.1.105", + "@opentui/solid": "0.1.105", "@pierre/diffs": "1.1.0-beta.18", "@playwright/test": "1.59.1", "@solid-primitives/storage": "4.3.3", @@ -760,13 +628,14 @@ "dompurify": "3.3.3", "drizzle-kit": "1.0.0-beta.19-d95b7a4", "drizzle-orm": "1.0.0-beta.19-d95b7a4", - "effect": "4.0.0-beta.48", + "effect": "4.0.0-beta.57", "fuzzysort": "3.1.0", "hono": "4.12.12", "hono-openapi": "1.1.2", "luxon": "3.6.1", "marked": "17.0.1", "marked-shiki": "1.2.1", + "opentui-spinner": "0.0.6", "remeda": "2.26.0", "remend": "1.3.0", "semver": "7.7.4", @@ -782,8 +651,6 @@ "zod": "4.1.8", }, "packages": { - "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], - "@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=="], @@ -966,8 +833,6 @@ "@azure/core-client": ["@azure/core-client@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="], - "@azure/core-http": ["@azure/core-http@3.0.5", "", { "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-tracing": "1.0.0-preview.13", "@azure/core-util": "^1.1.1", "@azure/logger": "^1.0.0", "@types/node-fetch": "^2.5.0", "@types/tunnel": "^0.0.3", "form-data": "^4.0.0", "node-fetch": "^2.6.7", "process": "^0.11.10", "tslib": "^2.2.0", "tunnel": "^0.0.6", "uuid": "^8.3.0", "xml2js": "^0.5.0" } }, "sha512-T8r2q/c3DxNu6mEJfPuJtptUVqwchxzjj32gKcnMi06rdiVONS9rar7kT9T2Am+XvER7uOzpsP79WsqNbdgdWg=="], - "@azure/core-http-compat": ["@azure/core-http-compat@2.4.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA=="], "@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="], @@ -1044,8 +909,6 @@ "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], - "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], - "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], @@ -1122,8 +985,6 @@ "@corvu/utils": ["@corvu/utils@0.4.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.11" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA=="], - "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], - "@dimforge/rapier2d-simd-compat": ["@dimforge/rapier2d-simd-compat@0.17.3", "", {}, "sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg=="], "@docsearch/css": ["@docsearch/css@4.6.2", "", {}, "sha512-fH/cn8BjEEdM2nJdjNMHIvOVYupG6AIDtFVDgIZrNzdCSj4KXr9kd+hsehqsNGYjpUjObeKYKvgy/IwCb1jZYQ=="], @@ -1134,28 +995,12 @@ "@effect/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="], - "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.48", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.48" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-vHk/X1vgDrviGcOTHQqzm2D81TtyPE/C7Qdksg5eAdbGpnqL4Dm4lk6PzTReQ0pO1/avIvWqpxy315IURV0Ldw=="], + "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.57", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.57" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-gdjZPEP0QQg4qmI1vd+443kheeQZKytrjJIzCJncy6ZEpyk/SfrqeStLqLXdTRcms3IB0ls0vOV7KNq7YmBRVA=="], - "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.48", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.48", "mime": "^4.1.0", "undici": "^8.0.2" }, "peerDependencies": { "effect": "^4.0.0-beta.48", "ioredis": "^5.7.0" } }, "sha512-8J6H0k9rtbp9O1QvKOyOPRcCTJ8WrR7IzZLJtYFTZ4bXVEEMCTo84h0CRpi7ccpA9t7DLqotip0NeFgiBosNKQ=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.57", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.57", "mime": "^4.1.0", "undici": "^8.0.2" }, "peerDependencies": { "effect": "^4.0.0-beta.57", "ioredis": "^5.7.0" } }, "sha512-la0xxPSAYOsY0d+uVxEBxok3jYB31iPQmIaZZRUj2SNWqcGGHJc6KorKtI8guqSLuv9FGZ255kBWXRbG6hMeeg=="], "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.46", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.46" } }, "sha512-Yzci82XbZ1W3tuiownsJawrJZTGeTrTZKLD0uxdBWCBzlVyqDwoSwRwO5qh33DurJj9B7iS8MDf14fpGRBPNGQ=="], - "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], - - "@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="], - - "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], - - "@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="], - - "@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="], - - "@electron/rebuild": ["@electron/rebuild@4.0.3", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", "graceful-fs": "^4.2.11", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^11.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA=="], - - "@electron/universal": ["@electron/universal@2.0.3", "", { "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="], - - "@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", "fs-extra": "^11.1.1", "minimist": "^1.2.8", "postject": "^1.0.0-alpha.6" }, "bin": { "electron-windows-sign": "bin/electron-windows-sign.js" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="], - "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -1262,8 +1107,6 @@ "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], - "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], - "@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.5", "", { "dependencies": { "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-f2ZHucnA2wBGAY8ipB4wn/mrEYW+WUxU2huJmUvfDO6AE2vfILSHeF3wCO39Pz4wUYPoAWZByaauftLrOfC12Q=="], "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="], @@ -1496,10 +1339,6 @@ "@lydell/node-pty-win32-x64": ["@lydell/node-pty-win32-x64@1.2.0-beta.10", "", { "os": "win32", "cpu": "x64" }, "sha512-ExFgWrzyldNAMi45U9PLIOu+g/RatP+f0c/dZxaooifME6yLW32BoHveH26/TtoAjZyJrc2iL0u48pgnR1fzmg=="], - "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], - - "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], - "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@types/node": "^12.7.1", "find-up": "^4.1.0", "fs-extra": "^8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="], "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], @@ -1620,11 +1459,7 @@ "@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/app": ["@opencode-ai/app@workspace:packages/app"], - - "@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"], - - "@opencode-ai/desktop-electron": ["@opencode-ai/desktop-electron@workspace:packages/desktop-electron"], + "@opencode-ai/core": ["@opencode-ai/core@workspace:packages/core"], "@opencode-ai/plugin": ["@opencode-ai/plugin@1.4.12", "", { "dependencies": { "@opencode-ai/sdk": "1.4.12", "effect": "4.0.0-beta.48", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.1.100", "@opentui/solid": ">=0.1.100" }, "optionalPeers": ["@opentui/core", "@opentui/solid"] }, "sha512-4CVk2NFnFltrOfnXqBEb6qY9aRxh6toBx68DDrFxGDnguyH3TtYT16Reyvm83TNAAtkkdOC2f14dGPhqt+u2CA=="], @@ -1632,8 +1467,6 @@ "@opencode-ai/sdk": ["@opencode-ai/sdk@1.4.12", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-V8/RnAPOcvvEOKefNBuWsrd1rY23bJUYVeI2CS1CEW9bUf0Q9JNESBT7Eop6jv77VBAwzljZju54AzgsCB7ydw=="], - "@opencode-ai/shared": ["@opencode-ai/shared@workspace:packages/shared"], - "@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"], "@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"], @@ -1668,21 +1501,21 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], - "@opentui/core": ["@opentui/core@0.1.99", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.99", "@opentui/core-darwin-x64": "0.1.99", "@opentui/core-linux-arm64": "0.1.99", "@opentui/core-linux-x64": "0.1.99", "@opentui/core-win32-arm64": "0.1.99", "@opentui/core-win32-x64": "0.1.99", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-I3+AEgGzqNWIpWX9g2WOscSPwtQDNOm4KlBjxBWCZjLxkF07u77heWXF7OiAdhKLtNUW6TFiyt6yznqAZPdG3A=="], + "@opentui/core": ["@opentui/core@0.1.105", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.105", "@opentui/core-darwin-x64": "0.1.105", "@opentui/core-linux-arm64": "0.1.105", "@opentui/core-linux-x64": "0.1.105", "@opentui/core-win32-arm64": "0.1.105", "@opentui/core-win32-x64": "0.1.105", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-vllSOOCW6VIThV/96GRLJ1IxIBuR+ci6FDvnPIAG4s7SJ/FW6zAkqDn1xrtBwwk/lM3QWjLqy8BZc+zwWvveJA=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.99", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bzVrqeX2vb5iWrc/ftOUOqeUY8XO+qSgoTwj5TXHuwagavgwD3Hpeyjx8+icnTTeM4pao0som1WR9xfye6/X5Q=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.105", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1pIL7aer9amwj8EpYoMNtvavKetIe+nX8uBRmYsMQb+KvJoUAZUqENfRW+qHE5WrsOyxx8/QoyXTHw15GG5iLQ=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.99", "", { "os": "darwin", "cpu": "x64" }, "sha512-VE4FrXBYpkxnvkqcCV1a8aN9jyyMJMihVW+V2NLCtp+4yQsj0AapG5TiUSN76XnmSZRptxDy5rBmEempeoIZbg=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.105", "", { "os": "darwin", "cpu": "x64" }, "sha512-hLIRSWlK3gY2NRXJGWiTBiMYSmRDjOYFZF6WtUVXhY2SL3sp08dhmr/6dmAVH+3pKCsCipLEsrrcQX6SAihCTA=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.99", "", { "os": "linux", "cpu": "arm64" }, "sha512-viXQsbpS7yHjYkl7+am32JdvG96QU9lvHh1UiZtpOxcNUUqiYmA2ZwZFPD2Bi54jNyj5l2hjH6YkD3DzE2FEWA=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.105", "", { "os": "linux", "cpu": "arm64" }, "sha512-jlRKfPkozTZEkHEePuCWYcTIUtPm+ieInAwGVqGmjbvqjxdVv1/W/Dt6LEZ/9jpRiOPd+FjXAfLe6wa/XWHr+w=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.99", "", { "os": "linux", "cpu": "x64" }, "sha512-WLoEFINOSp0tZSR9y4LUuGc7n4Y7H1wcpjUPzQ9vChkYDXrfZltEanzoDWbDcQ4kZQW5tHVC7LrZHpAsRLwFZg=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.105", "", { "os": "linux", "cpu": "x64" }, "sha512-kfWS1WMg6qHShmxZX9s1tZc/8JcXw6uyy2UtyTbJdRFExtXGH37oKHi8QK8iPL2ExCx4z7zqVnVJfO3X/Wh7lA=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.99", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWMOLWCEO8HdrctU1dMkgZC8qGkiO4Dwr4/e11tTvVpRmYhDsP/IR89ZjEEtOwnKwFOFuB/MxvflqaEWVQ2g5Q=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.105", "", { "os": "win32", "cpu": "arm64" }, "sha512-UFx6A8OpBVbGWK6OAw4GqAqKZgIITJfSOd35pG9yDVKQouHN2OGc2HeeXrH2A4h42p40Xl6IfcqqfllkpC13Dg=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.99", "", { "os": "win32", "cpu": "x64" }, "sha512-aYRlsL2w8YRL6vPd7/hrqlNVkXU3QowWb01TOvAcHS8UAsXaGFUr47kSDyjxDi1wg1MzmVduCfsC7T3NoThV1w=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.105", "", { "os": "win32", "cpu": "x64" }, "sha512-f9FqqUmxehwhF+cgyazm0YT0v0BYTTCPzd6eztqhl74N3x/kC+jOOz2rdJDC/tTBo1JVsF64KupOnhIs6/Cogg=="], - "@opentui/solid": ["@opentui/solid@0.1.99", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.99", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-DrqqO4h2V88FmeIP2cErYkMU0ZK5MrUsZw3w6IzZpoXyyiL4/9qpWzUq+CXx+r16VP2iGxDJwGKUmtFAzUch2Q=="], + "@opentui/solid": ["@opentui/solid@0.1.105", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.105", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-uxnaMP802sCI487pv/Hk9xdFdIj9mkg3eNliAqbqR0Shmd4phcjKEZvPRpijjmI99j4s9nul71jzF3h1oz31Nw=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -1964,8 +1797,6 @@ "@simple-git/argv-parser": ["@simple-git/argv-parser@1.1.1", "", { "dependencies": { "@simple-git/args-pathspec": "^1.0.3" } }, "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw=="], - "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], - "@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=="], @@ -2070,18 +1901,12 @@ "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], - "@solid-primitives/active-element": ["@solid-primitives/active-element@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9t5K4aR2naVDj950XU8OjnLgOg94a8k5wr6JNOPK+N5ESLsJDq42c1ZP8UKpewi1R+wplMMxiM6OPKRzbxJY7A=="], - - "@solid-primitives/audio": ["@solid-primitives/audio@1.4.2", "", { "dependencies": { "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMD3ORQfI5Ky8yuKPxidDiEazsjv/dsoiKK5yZxLnsgaeNR1Aym3/77h/qT1jBYeXUgj4DX6t7NMpFUSVr14OQ=="], - "@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=="], - "@solid-primitives/i18n": ["@solid-primitives/i18n@2.2.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TnTnE2Ku11MGYZ1JzhJ8pYscwg1fr9MteoYxPwsfxWfh9Jp5K7RRJncJn9BhOHvNLwROjqOHZ46PT7sPHqbcXw=="], - "@solid-primitives/keyed": ["@solid-primitives/keyed@1.5.3", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zNadtyYBhJSOjXtogkGHmRxjGdz9KHc8sGGVAGlUABkE8BED2tbIZoxkwSqzOwde8OcUEH0bb5DLZUWIMvyBSA=="], "@solid-primitives/lifecycle": ["@solid-primitives/lifecycle@0.1.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-+K0T10kZXqorocFj0coIqt8NYm2UqoZfpF3nm2RwrDMZMV+C+SC0Oi3N6Dnq2i7W/n1cHAnfpoV4CBLsW21lJw=="], @@ -2100,20 +1925,12 @@ "@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="], - "@solid-primitives/scroll": ["@solid-primitives/scroll@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ejq/Z7zKo/6eIEFr1bFLzXFxiGBCMLuqCM8QB8urr3YdPzjSETFLzYRWUyRiDWaBQN0F7k0SY6S7ig5nWOP7vg=="], - "@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-uxez7SXnr5GiRnzqO2IEDjOJRIXaG+0LZLBizmUA1FwSi+hrpuMzVBwyk70m4prcl8X6FDDXUl9O8hSq8wHbBQ=="], - "@solid-primitives/storage": ["@solid-primitives/storage@4.3.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12" }, "optionalPeers": ["@tauri-apps/plugin-store"] }, "sha512-ACbNwMZ1s8VAvld6EUXkDkX/US3IhtlPLxg6+B2s9MwNUugwdd51I98LPEaHrdLpqPmyzqgoJe0TxEFlf3Dqrw=="], - - "@solid-primitives/timer": ["@solid-primitives/timer@1.4.4", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ayjyb3+v1hyU92vuLUN0tVHq2mmTCPGxSDLGJMsDydRqx9ZfJIc9xj6cxK4XvdY3pif3ps2mIv52pjgToybEpQ=="], - "@solid-primitives/trigger": ["@solid-primitives/trigger@1.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Za2JebEiDyfamjmDwRaESYqBBYOlgYGzB8kHYH0QrkXyLf2qNADlKdGN+z3vWSLCTDcKxChS43Kssjuc0OZhng=="], "@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="], - "@solid-primitives/websocket": ["@solid-primitives/websocket@1.3.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-F06tA2FKa5VsnS4E4WEc3jHpsJfXRlMTGOtolugTzCqV3JmJTyvk9UVg1oz6PgGHKGi1CQ91OP8iW34myyJgaQ=="], - "@solidjs/meta": ["@solidjs/meta@0.29.4", "", { "peerDependencies": { "solid-js": ">=1.8.4" } }, "sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g=="], "@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="], @@ -2148,8 +1965,6 @@ "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], - "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="], @@ -2182,60 +1997,6 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="], - "@tanstack/query-core": ["@tanstack/query-core@5.91.2", "", {}, "sha512-Uz2pTgPC1mhqrrSGg18RKCWT/pkduAYtxbcyIyKBhw7dTWjXZIzqmpzO2lBkyWr4hlImQgpu1m1pei3UnkFRWw=="], - - "@tanstack/solid-query": ["@tanstack/solid-query@5.91.4", "", { "dependencies": { "@tanstack/query-core": "5.91.2" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-oCEgn8iT7WnF/7ISd7usBpUK1C9EdvQfg8ZUpKNKZ4edVClICZrCX6f3/Bp8ZlwQnL21KLc2rp+CejEuehlRxg=="], - - "@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="], - - "@tauri-apps/cli": ["@tauri-apps/cli@2.10.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.10.1", "@tauri-apps/cli-darwin-x64": "2.10.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", "@tauri-apps/cli-linux-arm64-musl": "2.10.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-musl": "2.10.1", "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", "@tauri-apps/cli-win32-x64-msvc": "2.10.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g=="], - - "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.10.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ=="], - - "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.10.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw=="], - - "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.10.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w=="], - - "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA=="], - - "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg=="], - - "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.10.1", "", { "os": "linux", "cpu": "none" }, "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw=="], - - "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw=="], - - "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ=="], - - "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.10.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg=="], - - "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.10.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw=="], - - "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg=="], - - "@tauri-apps/plugin-clipboard-manager": ["@tauri-apps/plugin-clipboard-manager@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ=="], - - "@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.8", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-Cd2Cs960MGuGONeIwxOPx9wqwedetAHOGlwK5boJ/SMTfAtAyfErpfVPEn+EJzgXsJun8EKzsEumHjr+64V4fw=="], - - "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.7.0", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-4nS/hfGMGCXiAS3LtVjH9AgsSAPJeG/7R+q8agTFqytjnMa4Zq95Bq8WzVDkckpanX+yyRHXnRtrKXkANKDHvw=="], - - "@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.8", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-oxd7oypzQeu8kAfFCrw534Kq7Cw+NzozcnCY21O4rz3A+veJiIiuSCMIprgGcZOcLAXFP9GmDhKUbhuKWcunRw=="], - - "@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg=="], - - "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ=="], - - "@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="], - - "@tauri-apps/plugin-process": ["@tauri-apps/plugin-process@2.3.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA=="], - - "@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.5", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg=="], - - "@tauri-apps/plugin-store": ["@tauri-apps/plugin-store@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A=="], - - "@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.10.1", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA=="], - - "@tauri-apps/plugin-window-state": ["@tauri-apps/plugin-window-state@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw=="], - "@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="], "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], @@ -2284,8 +2045,6 @@ "@types/cacache": ["@types/cacache@20.0.1", "", { "dependencies": { "@types/node": "*", "minipass": "*" } }, "sha512-QlKW3AFoFr/hvPHwFHMIVUH/ZCYeetBNou3PCmxu5LaNDvrtBlPJtIA6uhmU9JRt9oxj7IYoqoLcpxtzpPiTcw=="], - "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], - "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/command-line-args": ["@types/command-line-args@5.2.3", "", {}, "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw=="], @@ -2364,24 +2123,16 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="], - "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], - "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], - "@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="], - "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], - "@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=="], @@ -2420,8 +2171,6 @@ "@types/pacote": ["@types/pacote@11.1.8", "", { "dependencies": { "@types/node": "*", "@types/npm-registry-fetch": "*", "@types/npmlog": "*", "@types/ssri": "*" } }, "sha512-/XLR0VoTh2JEO0jJg1q/e6Rh9bxjBq9vorJuQmtT7rRrXSiWz7e7NsvXVYJQ0i8JxMlBMPPYDTnrRe7MZRFA8Q=="], - "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], - "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], @@ -2432,8 +2181,6 @@ "@types/readable-stream": ["@types/readable-stream@4.0.23", "", { "dependencies": { "@types/node": "*" } }, "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig=="], - "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], - "@types/sarif": ["@types/sarif@2.1.7", "", {}, "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ=="], "@types/scheduler": ["@types/scheduler@0.26.0", "", {}, "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA=="], @@ -2444,14 +2191,10 @@ "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], - "@types/tunnel": ["@types/tunnel@0.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA=="], - "@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="], - "@types/vscode": ["@types/vscode@1.116.0", "", {}, "sha512-sYHp4MO6BqJ2PD7Hjt0hlIS3tMaYsVPJrd0RUjDJ8HtOYnyJIEej0bLSccM8rE77WrC+Xox/kdBwEFDO8MsxNA=="], "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], @@ -2464,8 +2207,6 @@ "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], - "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/type-utils": "8.58.2", "@typescript-eslint/utils": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg=="], @@ -2508,6 +2249,8 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], + "@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="], "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], @@ -2560,8 +2303,6 @@ "@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="], - "@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="], - "@xterm/addon-clipboard": ["@xterm/addon-clipboard@0.2.0", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-Dl31BCtBhLaUEECUbEiVcCLvLBbaeGYdT7NofB8OJkGTD3MWgBsaLjXvfGAD4tQNHhm6mbKyYkR7XD8kiZsdNg=="], "@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="], @@ -2602,8 +2343,6 @@ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], - "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], @@ -2618,10 +2357,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=="], - "app-builder-bin": ["app-builder-bin@5.0.0-alpha.12", "", {}, "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w=="], - - "app-builder-lib": ["app-builder-lib@26.8.1", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.8.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.0.3", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.8.1", "electron-builder-squirrel-windows": "26.8.1" } }, "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw=="], - "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=="], @@ -2644,8 +2379,6 @@ "arrify": ["arrify@3.0.0", "", {}, "sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw=="], - "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], @@ -2654,20 +2387,14 @@ "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], - "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=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], - "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], - "atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="], - "autoprefixer": ["autoprefixer@10.5.0", "", { "dependencies": { "browserslist": "^4.28.2", "caniuse-lite": "^1.0.30001787", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong=="], "ava": ["ava@5.3.1", "", { "dependencies": { "acorn": "^8.8.2", "acorn-walk": "^8.2.0", "ansi-styles": "^6.2.1", "arrgv": "^1.0.2", "arrify": "^3.0.0", "callsites": "^4.0.0", "cbor": "^8.1.0", "chalk": "^5.2.0", "chokidar": "^3.5.3", "chunkd": "^2.0.1", "ci-info": "^3.8.0", "ci-parallel-vars": "^1.0.1", "clean-yaml-object": "^0.1.0", "cli-truncate": "^3.1.0", "code-excerpt": "^4.0.0", "common-path-prefix": "^3.0.0", "concordance": "^5.0.4", "currently-unhandled": "^0.4.1", "debug": "^4.3.4", "emittery": "^1.0.1", "figures": "^5.0.0", "globby": "^13.1.4", "ignore-by-default": "^2.1.0", "indent-string": "^5.0.0", "is-error": "^2.2.2", "is-plain-object": "^5.0.0", "is-promise": "^4.0.0", "matcher": "^5.0.0", "mem": "^9.0.2", "ms": "^2.1.3", "p-event": "^5.0.1", "p-map": "^5.5.0", "picomatch": "^2.3.1", "pkg-conf": "^4.0.0", "plur": "^5.1.0", "pretty-ms": "^8.0.0", "resolve-cwd": "^3.0.0", "stack-utils": "^2.0.6", "strip-ansi": "^7.0.1", "supertap": "^3.0.1", "temp-dir": "^3.0.0", "write-file-atomic": "^5.0.1", "yargs": "^17.7.2" }, "peerDependencies": { "@ava/typescript": "*" }, "optionalPeers": ["@ava/typescript"], "bin": { "ava": "entrypoints/cli.mjs" } }, "sha512-Scv9a4gMOXB6+ni4toLuhAm9KYWEjsgBglJl+kMGI5+IVDt120CCDZyB5HNU9DjmLI2t4I0GbnxGLmmRfGTJGg=="], @@ -2740,8 +2467,6 @@ "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], - "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], "boundary": ["boundary@2.0.0", "", {}, "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA=="], @@ -2762,14 +2487,8 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "buffers": ["buffers@0.1.1", "", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="], - "builder-util": ["builder-util@26.8.1", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "5.0.0-alpha.12", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw=="], - - "builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="], - "bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="], "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], @@ -2798,10 +2517,6 @@ "cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="], - "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], - - "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], - "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -2846,8 +2561,6 @@ "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], - "chunkd": ["chunkd@2.0.1", "", {}, "sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ=="], "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], @@ -2868,7 +2581,7 @@ "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], - "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], + "cli-truncate": ["cli-truncate@3.1.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" } }, "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA=="], "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], @@ -2876,10 +2589,6 @@ "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], - "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - - "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], - "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], "cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="], @@ -2910,16 +2619,12 @@ "common-path-prefix": ["common-path-prefix@3.0.0", "", {}, "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w=="], - "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], - "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=="], - "conf": ["conf@14.0.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^9.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.4.0" } }, "sha512-L6BuueHTRuJHQvQVc6YXYZRtN5vJUtOdCTLn0tRYYV5azfbAFcPghB5zEE40mVrV6w7slMTqUfkDomutIK14fw=="], - "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], @@ -2944,14 +2649,10 @@ "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], - "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], - "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-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="], - "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=="], @@ -3048,8 +2749,6 @@ "dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], - "debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], @@ -3068,16 +2767,10 @@ "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - - "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], - "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - "defu": ["defu@6.1.6", "", {}, "sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug=="], "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], @@ -3100,8 +2793,6 @@ "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], - "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], - "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], @@ -3110,14 +2801,8 @@ "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], - "dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="], - "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], - "dmg-builder": ["dmg-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg=="], - - "dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": { "dmg-license": "bin/dmg-license.js" } }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="], - "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], @@ -3132,12 +2817,8 @@ "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=="], - "dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="], - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], - "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=="], "drizzle-orm": ["drizzle-orm@1.0.0-beta.19-d95b7a4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-bZZKKeoRKrMVU6zKTscjrSH0+WNb1WEi3N0Jl4wEyQ7aQpTgHzdYY6IJQ1P0M74HuSJVeX4UpkFB/S6dtqLEJg=="], @@ -3152,38 +2833,10 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@4.0.0-beta.48", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw=="], - - "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - - "electron": ["electron@41.2.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-teeRThiYGTPKf/2yOW7zZA1bhb91KEQ4yLBPOg7GxpmnkLFLugKgQaAKOrCgdzwsXh/5mFIfmkm+4+wACJKwaA=="], - - "electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="], - - "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "electron-winstaller": "5.4.0" } }, "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA=="], - - "electron-context-menu": ["electron-context-menu@4.1.2", "", { "dependencies": { "cli-truncate": "^4.0.0", "electron-dl": "^4.0.0", "electron-is-dev": "^3.0.1" } }, "sha512-9xYTUV0oRqKL50N9W71IrXNdVRB0LuBp3R1zkUdUc2wfIa2/QZwYYj5RLuO7Tn7ZSLVIaO3X6u+EIBK+cBvzrQ=="], - - "electron-dl": ["electron-dl@4.0.0", "", { "dependencies": { "ext-name": "^5.0.0", "pupa": "^3.1.0", "unused-filename": "^4.0.1" } }, "sha512-USiB9816d2JzKv0LiSbreRfTg5lDk3lWh0vlx/gugCO92ZIJkHVH0UM18EHvKeadErP6Xn4yiTphWzYfbA2Ong=="], - - "electron-is-dev": ["electron-is-dev@3.0.1", "", {}, "sha512-8TjjAh8Ec51hUi3o4TaU0mD3GMTOESi866oRNavj9A3IQJ7pmv+MJVmdZBFGw4GFT36X7bkqnuDNYvkQgvyI8Q=="], - - "electron-log": ["electron-log@5.4.3", "", {}, "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ=="], - - "electron-publish": ["electron-publish@26.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="], - - "electron-store": ["electron-store@10.1.0", "", { "dependencies": { "conf": "^14.0.0", "type-fest": "^4.41.0" } }, "sha512-oL8bRy7pVCLpwhmXy05Rh/L6O93+k9t6dqSw0+MckIc3OmCTZm6Mp04Q4f/J0rtu84Ky6ywkR8ivtGOmrq+16w=="], + "effect": ["effect@4.0.0-beta.57", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-rg32VgXnLKaPRs9tbRDaZ5jxmzNY7ojXt85gSHGUTwdlbWH5Ik+OCUY2q14TXliygPGoHwCAvNWS4bQJOqf00g=="], "electron-to-chromium": ["electron-to-chromium@1.5.340", "", {}, "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA=="], - "electron-updater": ["electron-updater@6.8.3", "", { "dependencies": { "builder-util-runtime": "9.5.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ=="], - - "electron-vite": ["electron-vite@5.0.0", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-arrow-functions": "^7.27.1", "cac": "^6.7.14", "esbuild": "^0.25.11", "magic-string": "^0.30.19", "picocolors": "^1.1.1" }, "peerDependencies": { "@swc/core": "^1.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@swc/core"], "bin": { "electron-vite": "bin/electron-vite.js" } }, "sha512-OHp/vjdlubNlhNkPkL/+3JD34ii5ov7M0GpuXEVdQeqdQ3ulvVR7Dg/rNBLfS5XPIFwgoBLDf9sjjrL+CuDyRQ=="], - - "electron-window-state": ["electron-window-state@5.0.3", "", { "dependencies": { "jsonfile": "^4.0.0", "mkdirp": "^0.5.1" } }, "sha512-1mNTwCfkolXl3kMf50yW3vE2lZj0y92P/HYWFBrb+v2S/pCka5mdwN3cagKm458A7NjndSwijynXgcLWRodsVg=="], - - "electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="], - "emittery": ["emittery@1.2.1", "", {}, "sha512-sFz64DCRjirhwHLxofFqxYQm6DCp6o0Ix7jwKQvuCHPn4GMRZNuBZyLPu9Ccmk/QSCAMZt6FOUqA8JZCQvA9fw=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -3210,8 +2863,6 @@ "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], - "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -3222,16 +2873,12 @@ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], - "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], "esbuild-plugin-solid": ["esbuild-plugin-solid@0.6.0", "", { "dependencies": { "@babel/core": "^7.20.12", "@babel/preset-typescript": "^7.18.6", "babel-preset-solid": "^1.6.9" }, "peerDependencies": { "esbuild": ">=0.20", "solid-js": ">= 1.0" } }, "sha512-V1FvDALwLDX6K0XNYM9CMRAnMzA0+Ecu55qBUT9q/eAJh1KIDsTMFoOzMSgyHqbOfvrVfO3Mws3z7TW2GVnIZA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escape-goat": ["escape-goat@4.0.0", "", {}, "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg=="], - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -3286,20 +2933,12 @@ "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], - "ext-list": ["ext-list@2.2.2", "", { "dependencies": { "mime-db": "^1.28.0" } }, "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA=="], - - "ext-name": ["ext-name@5.0.0", "", { "dependencies": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" } }, "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ=="], - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], - "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], - - "extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], - "fast-check": ["fast-check@4.7.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ=="], "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], @@ -3352,8 +2991,6 @@ "file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="], - "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], @@ -3438,8 +3075,6 @@ "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], - "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#20bd361", {}, "anomalyco-ghostty-web-20bd361", "sha512-dW0nwaiBBcun9y5WJSvm3HxDLe5o9V0xLCndQvWonRVubU8CS1PHxZpLffyPt1YujPWC13ez03aWxcuKBPYYGQ=="], - "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], @@ -3452,12 +3087,8 @@ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], - "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], - "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], "google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], @@ -3466,8 +3097,6 @@ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - "got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="], - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "graphql": ["graphql@16.13.2", "", {}, "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig=="], @@ -3520,8 +3149,6 @@ "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], - "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "human-id": ["human-id@4.1.3", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q=="], @@ -3532,8 +3159,6 @@ "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], - "iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="], - "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=="], @@ -3558,8 +3183,6 @@ "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], @@ -3632,8 +3255,6 @@ "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], - "isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], "isomorphic-git": ["isomorphic-git@1.37.5", "", { "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", "pify": "^4.0.1", "readable-stream": "^4.0.0", "sha.js": "^2.4.12", "simple-get": "^4.0.1" }, "bin": { "isogit": "cli.cjs" } }, "sha512-wek54c5uFvd3WsxewLWt6h0GXKWQh0P8rRXns9bN1rHNjcgCb3+0lmyAsP594NeTtQFeCJQVS9b0kjbkD1l5qg=="], @@ -3650,8 +3271,6 @@ "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], - "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], - "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -3676,8 +3295,6 @@ "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], - "jschardet": ["jschardet@3.1.4", "", {}, "sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg=="], - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], @@ -3702,8 +3319,6 @@ "json-stringify-nice": ["json-stringify-nice@1.1.4", "", {}, "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw=="], - "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], - "json-with-bigint": ["json-with-bigint@3.5.8", "", {}, "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], @@ -3748,8 +3363,6 @@ "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], - "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], - "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=="], @@ -3786,8 +3399,6 @@ "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], - "linkifyjs": ["linkifyjs@4.3.2", "", {}, "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="], - "load-json-file": ["load-json-file@7.0.1", "", {}, "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], @@ -3800,16 +3411,12 @@ "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], - "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], - "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], - "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], - "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], @@ -3836,8 +3443,6 @@ "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], - "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], - "lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="], "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], @@ -3982,12 +3587,10 @@ "next": ["next@16.2.4", "", { "dependencies": { "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.4", "@next/swc-darwin-x64": "16.2.4", "@next/swc-linux-arm64-gnu": "16.2.4", "@next/swc-linux-arm64-musl": "16.2.4", "@next/swc-linux-x64-gnu": "16.2.4", "@next/swc-linux-x64-musl": "16.2.4", "@next/swc-win32-arm64-msvc": "16.2.4", "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q=="], - "node-abi": ["node-abi@4.28.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g=="], + "node-abi": ["node-abi@3.89.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA=="], "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], - "node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="], - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], @@ -4014,8 +3617,6 @@ "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], - "npm-bundled": ["npm-bundled@5.0.0", "", { "dependencies": { "npm-normalize-package-bin": "^5.0.0" } }, "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw=="], "npm-install-checks": ["npm-install-checks@8.0.0", "", { "dependencies": { "semver": "^7.1.1" } }, "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA=="], @@ -4042,8 +3643,6 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], - "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], "oidc-token-hash": ["oidc-token-hash@5.2.0", "", {}, "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw=="], @@ -4090,8 +3689,6 @@ "oxlint-tsgolint": ["oxlint-tsgolint@0.21.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.21.0", "@oxlint-tsgolint/darwin-x64": "0.21.0", "@oxlint-tsgolint/linux-arm64": "0.21.0", "@oxlint-tsgolint/linux-x64": "0.21.0", "@oxlint-tsgolint/win32-arm64": "0.21.0", "@oxlint-tsgolint/win32-x64": "0.21.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-HiWPhANwRnN1pZJQ2SgNB3WRR+1etLJHmRzQ/MJhyINsEIaOUCjxhlXJKbEaVUwdnyXwRWqo/P9Fx21lz0/mSg=="], - "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], - "p-defer": ["p-defer@1.0.0", "", {}, "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw=="], "p-event": ["p-event@5.0.1", "", { "dependencies": { "p-timeout": "^5.0.2" } }, "sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ=="], @@ -4150,8 +3747,6 @@ "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="], - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], @@ -4166,8 +3761,6 @@ "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], - "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], @@ -4202,8 +3795,6 @@ "playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="], - "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], - "plur": ["plur@5.1.0", "", { "dependencies": { "irregular-plurals": "^3.3.0" } }, "sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg=="], "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], @@ -4228,8 +3819,6 @@ "posthog-node": ["posthog-node@4.4.0", "", { "dependencies": { "axios": "^1.7.4" } }, "sha512-4FEFBuc4FDFkTEiCSIo7DFV0jpprlqfR/SzjVgg7E2rRvfxTW4J47IvCwSCo5C9lBeV0ujDubY/YArHG37aJIw=="], - "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], - "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], "preact": ["preact@10.29.1", "", {}, "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg=="], @@ -4256,16 +3845,10 @@ "proggy": ["proggy@4.0.0", "", {}, "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ=="], - "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], - "promise-all-reject-late": ["promise-all-reject-late@1.0.1", "", {}, "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw=="], "promise-call-limit": ["promise-call-limit@3.0.2", "", {}, "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw=="], - "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], - - "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], - "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], @@ -4280,8 +3863,6 @@ "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], - "pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="], - "pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="], "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], @@ -4322,8 +3903,6 @@ "read": ["read@1.0.7", "", { "dependencies": { "mute-stream": "~0.0.4" } }, "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ=="], - "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], - "read-cmd-shim": ["read-cmd-shim@6.0.0", "", {}, "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A=="], "read-pkg": ["read-pkg@9.0.1", "", { "dependencies": { "@types/normalize-package-data": "^2.4.3", "normalize-package-data": "^6.0.0", "parse-json": "^8.0.0", "type-fest": "^4.6.0", "unicorn-magic": "^0.1.0" } }, "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA=="], @@ -4366,38 +3945,26 @@ "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], - "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], - "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], - "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="], "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], - "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], "ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="], - "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - "rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], - "ripgrep": ["ripgrep@0.3.1", "", { "bin": { "rg": "lib/rg.mjs", "ripgrep": "lib/rg.mjs" } }, "sha512-6bDtNIBh1qPviVIU685/4uv0Ap5t8eS4wiJhy/tR2LdIeIey9CVasENlGS+ul3HnTmGANIp7AjnfsztsRmALfQ=="], - "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], - "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], "rollup": ["rollup@4.60.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", "@rollup/rollup-darwin-x64": "4.60.2", "@rollup/rollup-freebsd-arm64": "4.60.2", "@rollup/rollup-freebsd-x64": "4.60.2", "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", "@rollup/rollup-linux-arm-musleabihf": "4.60.2", "@rollup/rollup-linux-arm64-gnu": "4.60.2", "@rollup/rollup-linux-arm64-musl": "4.60.2", "@rollup/rollup-linux-loong64-gnu": "4.60.2", "@rollup/rollup-linux-loong64-musl": "4.60.2", "@rollup/rollup-linux-ppc64-gnu": "4.60.2", "@rollup/rollup-linux-ppc64-musl": "4.60.2", "@rollup/rollup-linux-riscv64-gnu": "4.60.2", "@rollup/rollup-linux-riscv64-musl": "4.60.2", "@rollup/rollup-linux-s390x-gnu": "4.60.2", "@rollup/rollup-linux-x64-gnu": "4.60.2", "@rollup/rollup-linux-x64-musl": "4.60.2", "@rollup/rollup-openbsd-x64": "4.60.2", "@rollup/rollup-openharmony-arm64": "4.60.2", "@rollup/rollup-win32-arm64-msvc": "4.60.2", "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ=="], @@ -4426,8 +3993,6 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="], - "sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -4440,8 +4005,6 @@ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], - "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], @@ -4496,8 +4059,6 @@ "simple-git": ["simple-git@3.35.2", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", "@simple-git/args-pathspec": "^1.0.2", "@simple-git/argv-parser": "^1.0.3", "debug": "^4.4.0" } }, "sha512-ZMjl06lzTm1EScxEGuM6+mEX+NQd14h/B3x0vWU+YOXAMF8sicyi1K4cjTfj5is+35ChJEHDl1EjypzYFWH2FA=="], - "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], - "simple-xml-to-json": ["simple-xml-to-json@1.2.7", "", {}, "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q=="], "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], @@ -4530,16 +4091,10 @@ "sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], - "sort-keys": ["sort-keys@1.1.2", "", { "dependencies": { "is-plain-obj": "^1.0.0" } }, "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg=="], - - "sort-keys-length": ["sort-keys-length@1.0.1", "", { "dependencies": { "sort-keys": "^1.0.0" } }, "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw=="], - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], @@ -4584,8 +4139,6 @@ "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], - "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], @@ -4596,8 +4149,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=="], - "stream-chat": ["stream-chat@9.38.0", "", { "dependencies": { "@types/jsonwebtoken": "^9.0.8", "@types/ws": "^8.5.14", "axios": "^1.12.2", "base64-js": "^1.5.1", "form-data": "^4.0.4", "isomorphic-ws": "^5.0.0", "jsonwebtoken": "^9.0.3", "linkifyjs": "^4.3.2", "ws": "^8.18.1" } }, "sha512-nyTFKHnhGfk1Op/xuZzPKzM9uNTy4TBma69+ApwGj/UtrK2pT6rSaU0Qy/oAqub+Bh7jR2/5vlV/8FWJ2BObFg=="], - "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=="], @@ -4630,16 +4181,10 @@ "structured-source": ["structured-source@4.0.0", "", { "dependencies": { "boundary": "^2.0.0" } }, "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA=="], - "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], - - "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], - "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], - "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], - "supertap": ["supertap@3.0.1", "", { "dependencies": { "indent-string": "^5.0.0", "js-yaml": "^3.14.1", "serialize-error": "^7.0.1", "strip-ansi": "^7.0.1" } }, "sha512-u1ZpIBCawJnO+0QePsEiOknOfCRq0yERxiAchT0i4li0WHNUJbf0evXXSXOcCAR4M8iMDoajXYmstm/qO81Isw=="], "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], @@ -4672,12 +4217,8 @@ "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="], - "temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="], - "temp-dir": ["temp-dir@3.0.0", "", {}, "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw=="], - "temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="], - "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], "terminal-link": ["terminal-link@4.0.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "supports-hyperlinks": "^3.2.0" } }, "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA=="], @@ -4698,12 +4239,8 @@ "time-zone": ["time-zone@1.0.0", "", {}, "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA=="], - "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], - "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], - "tiny-typed-emitter": ["tiny-typed-emitter@2.1.0", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], @@ -4720,8 +4257,6 @@ "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], - "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], - "to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -4738,8 +4273,6 @@ "traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="], - "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - "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=="], @@ -4750,8 +4283,6 @@ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], - "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], @@ -4804,8 +4335,6 @@ "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], - "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], - "ulid": ["ulid@3.0.1", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q=="], "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="], @@ -4816,10 +4345,6 @@ "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], - "unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="], - - "unique-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="], - "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], @@ -4838,8 +4363,6 @@ "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=="], - "unused-filename": ["unused-filename@4.0.1", "", { "dependencies": { "escape-string-regexp": "^5.0.0", "path-exists": "^5.0.0" } }, "sha512-ZX6U1J04K1FoSUeoX1OicAhw4d0aro2qo+L8RhJkiGTNtBNkd/Fi1Wxoc9HzcVu6HfOzm0si/N15JjxFmD1z6A=="], - "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=="], @@ -4852,8 +4375,6 @@ "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=="], - "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], - "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="], "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], @@ -4876,8 +4397,6 @@ "venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.37", "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.21" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-6SxA8a4MoA6Q/c+D3q7My0Hfog76enN3n0MXhwosM+tso66rXBEGeBRD/0lravRDVzL2Q1w5QJPc86rAVJtfXg=="], - "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], - "version-range": ["version-range@4.15.0", "", {}, "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], @@ -4912,8 +4431,6 @@ "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], @@ -4932,8 +4449,6 @@ "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="], - "which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], @@ -5056,12 +4571,6 @@ "@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=="], - "@azure/core-http/@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="], - - "@azure/core-http/@azure/core-tracing": ["@azure/core-tracing@1.0.0-preview.13", "", { "dependencies": { "@opentelemetry/api": "^1.0.1", "tslib": "^2.2.0" } }, "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ=="], - - "@azure/core-http/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], - "@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=="], "@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], @@ -5082,44 +4591,8 @@ "@changesets/write/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], - "@develar/schema-utils/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=="], - "@effect/platform-node/undici": ["undici@8.1.0", "", {}, "sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw=="], - "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], - - "@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "@electron/asar/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - - "@electron/fuses/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - - "@electron/get/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=="], - - "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - - "@electron/osx-sign/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], - - "@electron/rebuild/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "@electron/rebuild/node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="], - - "@electron/rebuild/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - - "@electron/rebuild/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=="], - - "@electron/universal/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], - - "@electron/universal/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - - "@electron/windows-sign/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/config-array/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], @@ -5192,8 +4665,6 @@ "@kilocode/kilo-indexing/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - "@manypkg/find-root/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "@manypkg/find-root/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=="], @@ -5250,20 +4721,6 @@ "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], - "@opencode-ai/app/@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="], - - "@opencode-ai/app/tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], - - "@opencode-ai/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@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-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="], - - "@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], - - "@opencode-ai/desktop-electron/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@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-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="], - - "@opencode-ai/desktop-electron/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], - - "@opencode-ai/desktop-electron/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], - "@opencode-ai/plugin/effect": ["effect@4.0.0-beta.46", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-3f6gXvvUMtEueCRY0tU76Vq2Pej1SAwwE+s0Owd5nD53yS5n4RZhUA1rlCGFuSbQFA225pGy8vO72+lpvu7u5A=="], "@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=="], @@ -5278,6 +4735,8 @@ "@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + "@opencode-ai/ui/mermaid": ["mermaid@11.14.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g=="], + "@opencode-ai/ui/tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], @@ -5312,8 +4771,6 @@ "@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=="], - "@solid-primitives/scroll/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="], - "@standard-community/standard-json/effect": ["effect@4.0.0-beta.46", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-3f6gXvvUMtEueCRY0tU76Vq2Pej1SAwwE+s0Owd5nD53yS5n4RZhUA1rlCGFuSbQFA225pGy8vO72+lpvu7u5A=="], "@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.46", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-3f6gXvvUMtEueCRY0tU76Vq2Pej1SAwwE+s0Owd5nD53yS5n4RZhUA1rlCGFuSbQFA225pGy8vO72+lpvu7u5A=="], @@ -5358,8 +4815,6 @@ "@textlint/linter-formatter/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], "@vscode/ripgrep/yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], @@ -5396,18 +4851,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=="], - "ajv-keywords/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=="], - - "app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="], - - "app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], - - "app-builder-lib/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "app-builder-lib/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], - - "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], - "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=="], @@ -5418,8 +4861,6 @@ "ava/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - "ava/cli-truncate": ["cli-truncate@3.1.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" } }, "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA=="], - "ava/globby": ["globby@13.2.2", "", { "dependencies": { "dir-glob": "^3.0.1", "fast-glob": "^3.3.0", "ignore": "^5.2.4", "merge2": "^1.4.1", "slash": "^4.0.0" } }, "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w=="], "ava/p-map": ["p-map@5.5.0", "", { "dependencies": { "aggregate-error": "^4.0.0" } }, "sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg=="], @@ -5444,12 +4885,6 @@ "buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "builder-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "builder-util/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "builder-util-runtime/sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], - "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], @@ -5460,8 +4895,6 @@ "c8/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "cacheable-request/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], - "chalk-template/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "cheerio/undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="], @@ -5470,7 +4903,7 @@ "clean-stack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], + "cli-truncate/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=="], "command-line-usage/array-back": ["array-back@6.2.3", "", {}, "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw=="], @@ -5478,10 +4911,6 @@ "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - "conf/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], - - "crc/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "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=="], @@ -5494,18 +4923,8 @@ "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - - "dir-compare/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - "dir-glob/path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - "dmg-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "dmg-builder/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "dmg-license/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=="], - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "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=="], @@ -5514,22 +4933,6 @@ "effect/uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], - "electron-builder/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "electron-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "electron-builder/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "electron-publish/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "electron-publish/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "electron-publish/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], - - "electron-updater/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "electron-vite/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=="], - "encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], @@ -5546,10 +4949,6 @@ "eslint/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], - - "extract-zip/yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], @@ -5558,8 +4957,6 @@ "figures/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], - "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], - "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "framer-motion/motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="], @@ -5576,18 +4973,10 @@ "gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "global-agent/matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], - "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "gray-matter/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], - "http2-wrapper/quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], - - "iconv-corefoundation/cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], - - "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], - "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "isomorphic-git/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -5700,22 +5089,14 @@ "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], - "plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], - "posthog-js/@opentelemetry/resources": ["@opentelemetry/resources@2.7.0", "", { "dependencies": { "@opentelemetry/core": "2.7.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A=="], - "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], - "prebuild-install/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "prebuild-install/node-abi": ["node-abi@3.89.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA=="], - "pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], @@ -5736,8 +5117,6 @@ "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "secretlint/globby": ["globby@14.1.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", "fast-glob": "^3.3.3", "ignore": "^7.0.3", "path-type": "^6.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.3.0" } }, "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA=="], "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], @@ -5752,8 +5131,6 @@ "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], - "sort-keys/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], - "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=="], "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], @@ -5788,28 +5165,18 @@ "tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "temp-file/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - "test-exclude/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=="], - "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], "token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "tree-sitter-bash/node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="], - "unused-filename/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - - "unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], - "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=="], - "verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], - "vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], "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=="], @@ -5862,62 +5229,6 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@develar/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], - - "@electron/fuses/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "@electron/fuses/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "@electron/fuses/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "@electron/notarize/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "@electron/notarize/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "@electron/osx-sign/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "@electron/osx-sign/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "@electron/rebuild/node-gyp/make-fetch-happen": ["make-fetch-happen@14.0.3", "", { "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" } }, "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ=="], - - "@electron/rebuild/node-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], - - "@electron/rebuild/node-gyp/proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="], - - "@electron/rebuild/node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], - - "@electron/rebuild/ora/bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - - "@electron/rebuild/ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "@electron/rebuild/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "@electron/rebuild/ora/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "@electron/rebuild/ora/is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], - - "@electron/rebuild/ora/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], - - "@electron/rebuild/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@electron/rebuild/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=="], - - "@electron/rebuild/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@electron/rebuild/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "@electron/universal/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "@electron/universal/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], - - "@electron/windows-sign/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "@electron/windows-sign/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -5948,10 +5259,6 @@ "@kilocode/kilo-gateway/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="], - "@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "@malept/flatpak-bundler/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "@morphllm/morphsdk/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="], @@ -5990,12 +5297,6 @@ "@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], - "@opencode-ai/app/@playwright/test/playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="], - - "@opencode-ai/desktop-electron/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], - - "@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], - "@opencode-ai/plugin/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@opencode-ai/plugin/effect/toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], @@ -6012,6 +5313,12 @@ "@opencode-ai/ui/@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=="], + "@opencode-ai/ui/mermaid/dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], + + "@opencode-ai/ui/mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + + "@opencode-ai/ui/mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@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=="], "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], @@ -6116,20 +5423,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=="], - "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "app-builder-lib/@electron/get/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=="], - - "app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "app-builder-lib/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "app-builder-lib/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "app-builder-lib/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - - "app-builder-lib/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], - "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=="], @@ -6140,8 +5433,6 @@ "ava/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "ava/cli-truncate/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=="], - "ava/globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "ava/globby/slash": ["slash@4.0.0", "", {}, "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew=="], @@ -6162,12 +5453,6 @@ "bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "builder-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "builder-util/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "builder-util/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "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=="], @@ -6176,7 +5461,7 @@ "chalk-template/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "crc/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "cli-truncate/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -6184,16 +5469,6 @@ "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], - "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], - - "dir-compare/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - - "dmg-builder/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "dmg-builder/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "dmg-license/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], @@ -6246,80 +5521,6 @@ "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "electron-builder/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "electron-builder/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "electron-builder/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "electron-builder/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=="], - - "electron-builder/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "electron-builder/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "electron-publish/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "electron-publish/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "electron-publish/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "electron-updater/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "electron-updater/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "electron-vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "electron-vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "electron-vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "electron-vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "electron-vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "electron-vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "electron-vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "electron-vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "electron-vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "electron-vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "electron-vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "electron-vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "electron-vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "electron-vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "electron-vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "electron-vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "electron-vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "electron-vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "electron-vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "electron-vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "electron-vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "electron-vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "electron-vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "electron-vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "electron-vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "electron-vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -6328,10 +5529,6 @@ "eslint/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], - "extract-zip/yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], - - "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "friendly-words/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], @@ -6360,10 +5557,6 @@ "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "iconv-corefoundation/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="], - - "iconv-corefoundation/cli-truncate/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], @@ -6426,8 +5619,6 @@ "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], - "rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "secretlint/globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -6442,10 +5633,6 @@ "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=="], - "temp-file/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "temp-file/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "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=="], "test-exclude/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -6464,40 +5651,6 @@ "@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=="], - "@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], - - "@electron/rebuild/node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], - - "@electron/rebuild/node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], - - "@electron/rebuild/ora/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - - "@electron/rebuild/ora/bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "@electron/rebuild/ora/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "@electron/rebuild/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "@electron/rebuild/ora/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@electron/rebuild/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@electron/rebuild/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=="], - - "@electron/rebuild/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@electron/rebuild/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -6518,10 +5671,6 @@ "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], - "@opencode-ai/app/@playwright/test/playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], - - "@opencode-ai/app/@playwright/test/playwright/playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], - "@tailwindcss/vite/@tailwindcss/node/lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], @@ -6586,8 +5735,6 @@ "archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "ava/cli-truncate/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "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=="], @@ -6610,20 +5757,8 @@ "c8/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "dir-compare/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "electron-builder/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "electron-builder/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=="], - - "electron-builder/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "electron-builder/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "friendly-words/express/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "friendly-words/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], @@ -6642,10 +5777,6 @@ "gray-matter/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], - "iconv-corefoundation/cli-truncate/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "iconv-corefoundation/cli-truncate/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "mocha/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=="], "mocha/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -6682,8 +5813,6 @@ "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], - "supertap/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], "tar-fs/tar-stream/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], @@ -6694,26 +5823,6 @@ "test-exclude/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "@electron/rebuild/node-gyp/make-fetch-happen/@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/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=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch/minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="], - - "@electron/rebuild/ora/bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "@electron/rebuild/ora/cli-cursor/restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "@electron/rebuild/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "@electron/rebuild/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@electron/rebuild/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@manypkg/find-root/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], @@ -6744,16 +5853,10 @@ "c8/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "electron-builder/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "electron-builder/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "friendly-words/express/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "friendly-words/express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "iconv-corefoundation/cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "mocha/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=="], "mocha/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=="], @@ -6772,8 +5875,6 @@ "qrcode/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "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=="], @@ -6782,16 +5883,6 @@ "test-exclude/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/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=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch/minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - - "@electron/rebuild/ora/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "@vscode/test-cli/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@vscode/test-cli/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -6809,19 +5900,5 @@ "test-exclude/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "test-exclude/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/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=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/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=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/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=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], } } diff --git a/flake.nix b/flake.nix index 6380c23d272..541454f9746 100644 --- a/flake.nix +++ b/flake.nix @@ -267,13 +267,9 @@ opencode = final.callPackage ./nix/opencode.nix { inherit node_modules; }; - desktop = final.callPackage ./nix/desktop.nix { - inherit opencode; - }; in { inherit opencode; - opencode-desktop = desktop; }; }; @@ -286,13 +282,10 @@ kilo = pkgs.callPackage ./nix/kilo.nix { inherit node_modules; }; - desktop = pkgs.callPackage ./nix/desktop.nix { - inherit kilo; - }; in { default = kilo; - inherit kilo desktop; + inherit kilo; # Updater derivation with fakeHash - build fails and reveals correct hash node_modules_updater = node_modules.override { hash = pkgs.lib.fakeHash; diff --git a/install b/install index c4186688256..c6d0e37e0b6 100755 --- a/install +++ b/install @@ -385,6 +385,57 @@ add_to_path() { fi } +# Persistently add $INSTALL_DIR to the Windows user PATH via the registry. +# Works from Git Bash (MINGW64), MSYS2, and Cygwin. +# Converts the MSYS/Cygwin path to native Windows form so that cmd and +# PowerShell also see the entry. +add_to_windows_path() { + local win_install_dir + if command -v cygpath >/dev/null 2>&1; then + win_install_dir=$(cygpath -w "$INSTALL_DIR") + else + # Fallback: naive conversion /c/Users/... -> C:\Users\... + win_install_dir=$(echo "$INSTALL_DIR" | sed -e 's|^/\([a-zA-Z]\)/|\1:\\|' -e 's|/|\\|g') + fi + + # Escape single-quotes for PowerShell single-quoted strings ('' is the escape sequence) + local win_install_dir_ps="${win_install_dir//\'/\'\'}" + + local ps_cmd + ps_cmd=$(cat </dev/null 2>&1; then + ps_output=$(powershell.exe -NoProfile -NonInteractive -Command "$ps_cmd" 2>/dev/null) || return 1 + elif command -v pwsh >/dev/null 2>&1; then + ps_output=$(pwsh -NoProfile -NonInteractive -Command "$ps_cmd" 2>/dev/null) || return 1 + else + return 1 + fi + + if [[ "$ps_output" == *"added"* ]]; then + print_message info "${MUTED}Successfully added ${NC}kilo ${MUTED}to Windows user PATH${NC}" + fi + + # Also add to the current bash session so `kilo` works immediately + # without the user needing to open a new terminal. + if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then + export PATH="$INSTALL_DIR:$PATH" + fi + + return 0 +} + XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config} current_shell=$(basename "$SHELL") @@ -411,6 +462,13 @@ case $current_shell in esac if [[ "$no_modify_path" != "true" ]]; then + # Detect if we are running on Windows (MINGW, MSYS, Cygwin) regardless + # of whether the binary was installed from download or --binary. + _running_on_windows=false + case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) _running_on_windows=true ;; + esac + config_file="" for file in $config_files; do if [[ -f $file ]]; then @@ -419,7 +477,12 @@ if [[ "$no_modify_path" != "true" ]]; then fi done - if [[ -z $config_file ]]; then + if [[ "$_running_on_windows" == "true" ]] && add_to_windows_path; then + # Handled via the Windows user-level PATH in the registry. + # The change is visible in new cmd / PowerShell / Git Bash sessions. + # We also exported it into the current session above. + : + elif [[ -z $config_file ]]; then print_message warning "No config file found for $current_shell. You may need to manually add to PATH:" print_message info " export PATH=$INSTALL_DIR:\$PATH" elif [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then diff --git a/kilocode-2.code-workspace b/kilocode-2.code-workspace new file mode 100644 index 00000000000..0efadfb81b0 --- /dev/null +++ b/kilocode-2.code-workspace @@ -0,0 +1,30 @@ +{ + "folders": [ + { + "path": ".", + }, + ], + "settings": { + "window.title": "[main] Fix QuestionDock clickable options", + "peacock.color": "#ff3d00", + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#ff6433", + "activityBar.background": "#ff6433", + "activityBar.foreground": "#15202b", + "activityBar.inactiveForeground": "#15202b99", + "activityBarBadge.background": "#00ff3d", + "activityBarBadge.foreground": "#15202b", + "commandCenter.border": "#e7e7e799", + "sash.hoverBorder": "#ff6433", + "statusBar.background": "#ff3d00", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#ff6433", + "statusBarItem.remoteBackground": "#ff3d00", + "statusBarItem.remoteForeground": "#e7e7e7", + "titleBar.activeBackground": "#ff3d00", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#ff3d0099", + "titleBar.inactiveForeground": "#e7e7e799" + }, + }, +} diff --git a/nix/desktop.nix b/nix/desktop.nix deleted file mode 100644 index 9bc5734d5bc..00000000000 --- a/nix/desktop.nix +++ /dev/null @@ -1,100 +0,0 @@ -{ - lib, - stdenv, - rustPlatform, - pkg-config, - cargo-tauri, - bun, - nodejs, - cargo, - rustc, - jq, - wrapGAppsHook4, - makeWrapper, - dbus, - glib, - gtk4, - libsoup_3, - librsvg, - libappindicator, - glib-networking, - openssl, - webkitgtk_4_1, - gst_all_1, - kilo, -}: -rustPlatform.buildRustPackage (finalAttrs: { - pname = "kilo-desktop"; - inherit (kilo) - version - src - node_modules - patches - ; - - cargoRoot = "packages/desktop/src-tauri"; - cargoLock.lockFile = ../packages/desktop/src-tauri/Cargo.lock; - buildAndTestSubdir = finalAttrs.cargoRoot; - - nativeBuildInputs = [ - pkg-config - cargo-tauri.hook - bun - nodejs # for patchShebangs node_modules - cargo - rustc - jq - makeWrapper - ] ++ lib.optionals stdenv.hostPlatform.isLinux [ wrapGAppsHook4 ]; - - buildInputs = lib.optionals stdenv.isLinux [ - dbus - glib - gtk4 - libsoup_3 - librsvg - libappindicator - glib-networking - openssl - webkitgtk_4_1 - gst_all_1.gstreamer - gst_all_1.gst-plugins-base - gst_all_1.gst-plugins-good - gst_all_1.gst-plugins-bad - ]; - - strictDeps = true; - - preBuild = '' - cp -a ${finalAttrs.node_modules}/{node_modules,packages} . - chmod -R u+w node_modules packages - patchShebangs node_modules - patchShebangs packages/desktop/node_modules - - mkdir -p packages/desktop/src-tauri/sidecars - cp ${kilo}/bin/kilo packages/desktop/src-tauri/sidecars/kilo-cli-${stdenv.hostPlatform.rust.rustcTarget} - ''; - - # see publish-tauri job in .github/workflows/publish.yml - tauriBuildFlags = [ - "--config" - "tauri.prod.conf.json" - "--no-sign" # no code signing or auto updates - ]; - - # FIXME: workaround for concerns about case insensitive filesystems - # should be removed once binary is renamed or decided otherwise - # darwin output is a .app bundle so no conflict - postFixup = lib.optionalString stdenv.hostPlatform.isLinux '' - mv $out/bin/Kilo $out/bin/kilo-desktop - sed -i 's|^Exec=Kilo$|Exec=kilo-desktop|' $out/share/applications/Kilo.desktop - ''; - - meta = { - description = "Kilo Desktop App"; - homepage = "https://kilo.ai"; - license = lib.licenses.mit; - mainProgram = "kilo-desktop"; - inherit (kilo.meta) platforms; - }; -}) diff --git a/nix/hashes.json b/nix/hashes.json index 7e5e025b455..46fa567f4c9 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-4hLG583KyBLxAOYvIiVMh+xLtVAdR3QgWFEqnGlZLZw=", - "aarch64-linux": "sha256-+60DxQ/GKAAoMFuAQRFoYM0HlZMzpLpLrTD7qmweUUA=", - "aarch64-darwin": "sha256-3WCWVqaxu+lCIc51N7wOttM/hoXbbnmtw7EP2eGstQg=", - "x86_64-darwin": "sha256-K8iG+OHkgC/lwud6hQ91sXF3c7UbdQQYRU/KK2P+B/4=" + "x86_64-linux": "sha256-bTa0SN4bOmJTi8jrbksF9x7rHN7/4ih0R73liabbAgU=", + "aarch64-linux": "sha256-CLRpU85zXLKPdk0uHY77EDRZkH/L0DJiAkpg/agpvtE=", + "aarch64-darwin": "sha256-Vqt4NWbeDC44aWSchepIMN2p56Vq+Hlmj8xYMZNdc8w=", + "x86_64-darwin": "sha256-R5iv/ysbtm3nH8YzB84clPEFTjJydD/8Tt3Brxzn4RM=" } } diff --git a/nix/kilo.nix b/nix/kilo.nix index 6c6375405f3..9d01a0ee26a 100644 --- a/nix/kilo.nix +++ b/nix/kilo.nix @@ -64,7 +64,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { [ ripgrep ] - # bun runs sysctl to detect if dunning on rosetta2 + # bun runs sysctl to detect if running on rosetta2 ++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl ) } diff --git a/nix/node_modules.nix b/nix/node_modules.nix index e92a8a48849..26b7364408b 100644 --- a/nix/node_modules.nix +++ b/nix/node_modules.nix @@ -30,7 +30,7 @@ stdenvNoCC.mkDerivation { ../bun.lock ../package.json ../patches - ../install # required by desktop build (cli.rs include_str!) + ../install ] ); }; @@ -52,9 +52,6 @@ stdenvNoCC.mkDerivation { --os="${bunOs}" \ --filter '!./' \ --filter './packages/opencode' \ - --filter './packages/desktop' \ - --filter './packages/app' \ - --filter './packages/shared' \ --frozen-lockfile \ --ignore-scripts \ --no-progress diff --git a/package.json b/package.json index d108390d829..d1008676a0f 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,10 @@ "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev-setup": "bun run --cwd packages/opencode --conditions=browser src/index.ts dev-setup", - "dev:desktop": "bun --cwd packages/desktop-electron dev", - "dev:web": "bun --cwd packages/app dev", - "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", "dev:storybook": "bun --cwd packages/storybook storybook", "lint": "oxlint", "typecheck": "bun turbo typecheck", - "postinstall": "bun run --cwd packages/opencode fix-node-pty", + "postinstall": "bun run --cwd packages/opencode fix-node-pty && bun run script/setup-git.ts", "prepare": "husky", "random": "echo 'Random script'", "hello": "echo 'Hello World!'", @@ -27,15 +24,15 @@ "packages/sdk/js" ], "catalog": { - "@effect/opentelemetry": "4.0.0-beta.48", - "@effect/platform-node": "4.0.0-beta.48", + "@effect/opentelemetry": "4.0.0-beta.57", + "@effect/platform-node": "4.0.0-beta.57", "@npmcli/arborist": "9.4.0", "@types/bun": "1.3.12", "@types/cross-spawn": "6.0.6", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "0.1.105", + "@opentui/solid": "0.1.105", "ulid": "3.0.1", "@kobalte/core": "0.13.11", "@types/luxon": "3.7.1", @@ -46,13 +43,14 @@ "@cloudflare/workers-types": "4.20251008.0", "@openauthjs/openauth": "0.0.0-20250322224806", "@pierre/diffs": "1.1.0-beta.18", + "opentui-spinner": "0.0.6", "@solid-primitives/storage": "4.3.3", "@tailwindcss/vite": "4.1.11", "diff": "8.0.4", "dompurify": "3.3.3", "drizzle-kit": "1.0.0-beta.19-d95b7a4", "drizzle-orm": "1.0.0-beta.19-d95b7a4", - "effect": "4.0.0-beta.48", + "effect": "4.0.0-beta.57", "ai": "6.0.168", "cross-spawn": "7.0.6", "hono": "4.12.12", @@ -145,9 +143,8 @@ "patchedDependencies": { "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", - "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", - "stream-chat@9.38.0": "patches/stream-chat@9.38.0.patch" + "solid-js@1.9.10": "patches/solid-js@1.9.10.patch" }, - "version": "7.2.33", + "version": "7.2.42", "peerDependencies": {} } diff --git a/packages/app/.gitignore b/packages/app/.gitignore deleted file mode 100644 index d699efb38d2..00000000000 --- a/packages/app/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -src/assets/theme.css -e2e/test-results -e2e/playwright-report diff --git a/packages/app/AGENTS.md b/packages/app/AGENTS.md deleted file mode 100644 index 765e960c817..00000000000 --- a/packages/app/AGENTS.md +++ /dev/null @@ -1,30 +0,0 @@ -## Debugging - -- NEVER try to restart the app, or the server process, EVER. - -## Local Dev - -- `opencode dev web` proxies `https://app.opencode.ai`, so local UI/CSS changes will not show there. -- For local UI changes, run the backend and app dev servers separately. -- Backend (from `packages/opencode`): `bun run --conditions=browser ./src/index.ts serve --port 4096` -- App (from `packages/app`): `bun dev -- --port 4444` -- Open `http://localhost:4444` to verify UI changes (it targets the backend at `http://localhost:4096`). - -## SolidJS - -- Always prefer `createStore` over multiple `createSignal` calls - -## Tool Calling - -- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. - -## Browser Automation - -Use `agent-browser` for web automation. Run `agent-browser --help` for all commands. - -Core workflow: - -1. `agent-browser open ` - Navigate to page -2. `agent-browser snapshot -i` - Get interactive elements with refs (@e1, @e2) -3. `agent-browser click @e1` / `fill @e2 "text"` - Interact using refs -4. Re-snapshot after page changes diff --git a/packages/app/README.md b/packages/app/README.md deleted file mode 100644 index 304e272cd0a..00000000000 --- a/packages/app/README.md +++ /dev/null @@ -1,50 +0,0 @@ -## Usage - -Dependencies for these templates are managed with [pnpm](https://pnpm.io) using `pnpm up -Lri`. - -This is the reason you see a `pnpm-lock.yaml`. That said, any package manager will work. This file can safely be removed once you clone a template. - -```bash -$ npm install # or pnpm install or yarn install -``` - -### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs) - -## Available Scripts - -In the project directory, you can run: - -### `npm run dev` or `npm start` - -Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.
- -### `npm run build` - -Builds the app for production to the `dist` folder.
-It correctly bundles Solid in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.
-Your app is ready to be deployed! - -## E2E Testing - -Playwright starts the Vite dev server automatically via `webServer`, and UI tests expect an opencode backend at `localhost:4096` by default. - -```bash -bunx playwright install chromium -bun run test:e2e:local -bun run test:e2e:local -- --grep "settings" -``` - -Environment options: - -- `PLAYWRIGHT_SERVER_HOST` / `PLAYWRIGHT_SERVER_PORT` (backend address, default: `localhost:4096`) -- `PLAYWRIGHT_PORT` (Vite dev server port, default: `3000`) -- `PLAYWRIGHT_BASE_URL` (override base URL, default: `http://localhost:`) - -## Deployment - -You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.) diff --git a/packages/app/bunfig.toml b/packages/app/bunfig.toml deleted file mode 100644 index f1caabbcce9..00000000000 --- a/packages/app/bunfig.toml +++ /dev/null @@ -1,3 +0,0 @@ -[test] -root = "./src" -preload = ["./happydom.ts"] diff --git a/packages/app/create-effect-simplification-spec.md b/packages/app/create-effect-simplification-spec.md deleted file mode 100644 index cc101ab0592..00000000000 --- a/packages/app/create-effect-simplification-spec.md +++ /dev/null @@ -1,515 +0,0 @@ -# CreateEffect Simplification Implementation Spec - -Reduce reactive misuse across `packages/app`. - ---- - -## Context - -This work targets `packages/app/src`, which currently has 101 `createEffect` calls across 37 files. - -The biggest clusters are `pages/session.tsx` (19), `pages/layout.tsx` (13), `pages/session/file-tabs.tsx` (6), and several context providers that mirror one store into another. - -Key issues from the audit: - -- Derived state is being written through effects instead of computed directly -- Session and file resets are handled by watch-and-clear effects instead of keyed state boundaries -- User-driven actions are hidden inside reactive effects -- Context layers mirror and hydrate child stores with multiple sync effects -- Several areas repeat the same imperative trigger pattern in multiple effects - -Keep the implementation focused on removing unnecessary effects, not on broad UI redesign. - -## Goals - -- Cut high-churn `createEffect` usage in the hottest files first -- Replace effect-driven derived state with reactive derivation -- Replace reset-on-key effects with keyed ownership boundaries -- Move event-driven work to direct actions and write paths -- Remove mirrored store hydration where a single source of truth can exist -- Leave necessary external sync effects in place, but make them narrower and clearer - -## Non-Goals - -- Do not rewrite unrelated component structure just to reduce the count -- Do not change product behavior, navigation flow, or persisted data shape unless required for a cleaner write boundary -- Do not remove effects that bridge to DOM, editors, polling, or external APIs unless there is a clearly safer equivalent -- Do not attempt a repo-wide cleanup outside `packages/app` - -## Effect Taxonomy And Replacement Rules - -Use these rules during implementation. - -### Prefer `createMemo` - -Use `createMemo` when the target value is pure derived state from other signals or stores. - -Do this when an effect only reads reactive inputs and writes another reactive value that could be computed instead. - -Apply this to: - -- `packages/app/src/pages/session.tsx:141` -- `packages/app/src/pages/layout.tsx:557` -- `packages/app/src/components/terminal.tsx:261` -- `packages/app/src/components/session/session-header.tsx:309` - -Rules: - -- If no external system is touched, do not use `createEffect` -- Derive once, then read the memo where needed -- If normalization is required, prefer normalizing at the write boundary before falling back to a memo - -### Prefer Keyed Remounts - -Use keyed remounts when local UI state should reset because an identity changed. - -Do this with `sessionKey`, `scope()`, or another stable identity instead of watching the key and manually clearing signals. - -Apply this to: - -- `packages/app/src/pages/session.tsx:325` -- `packages/app/src/pages/session.tsx:336` -- `packages/app/src/pages/session.tsx:477` -- `packages/app/src/pages/session.tsx:869` -- `packages/app/src/pages/session.tsx:963` -- `packages/app/src/pages/session/message-timeline.tsx:149` -- `packages/app/src/context/file.tsx:100` - -Rules: - -- If the desired behavior is "new identity, fresh local state," key the owner subtree -- Keep state local to the keyed boundary so teardown and recreation handle the reset naturally - -### Prefer Event Handlers And Actions - -Use direct handlers, store actions, and async command functions when work happens because a user clicked, selected, reloaded, or navigated. - -Do this when an effect is just watching for a flag change, command token, or event-bus signal to trigger imperative logic. - -Apply this to: - -- `packages/app/src/pages/layout.tsx:484` -- `packages/app/src/pages/layout.tsx:652` -- `packages/app/src/pages/layout.tsx:776` -- `packages/app/src/pages/layout.tsx:1489` -- `packages/app/src/pages/layout.tsx:1519` -- `packages/app/src/components/file-tree.tsx:328` -- `packages/app/src/pages/session/terminal-panel.tsx:55` -- `packages/app/src/context/global-sync.tsx:148` -- Duplicated trigger sets in: - - `packages/app/src/pages/session/review-tab.tsx:122` - - `packages/app/src/pages/session/review-tab.tsx:130` - - `packages/app/src/pages/session/review-tab.tsx:138` - - `packages/app/src/pages/session/file-tabs.tsx:367` - - `packages/app/src/pages/session/file-tabs.tsx:378` - - `packages/app/src/pages/session/file-tabs.tsx:389` - - `packages/app/src/pages/session/use-session-hash-scroll.ts:144` - - `packages/app/src/pages/session/use-session-hash-scroll.ts:149` - - `packages/app/src/pages/session/use-session-hash-scroll.ts:167` - -Rules: - -- If the trigger is user intent, call the action at the source of that intent -- If the same imperative work is triggered from multiple places, extract one function and call it directly - -### Prefer `onMount` And `onCleanup` - -Use `onMount` and `onCleanup` for lifecycle-only setup and teardown. - -This is the right fit for subscriptions, one-time wiring, timers, and imperative integration that should not rerun for ordinary reactive changes. - -Use this when: - -- Setup should happen once per owner lifecycle -- Cleanup should always pair with teardown -- The work is not conceptually derived state - -### Keep `createEffect` When It Is A Real Bridge - -Keep `createEffect` when it synchronizes reactive data to an external imperative sink. - -Examples that should remain, though they may be narrowed or split: - -- DOM/editor sync in `packages/app/src/components/prompt-input.tsx:690` -- Scroll sync in `packages/app/src/pages/session.tsx:685` -- Scroll/hash sync in `packages/app/src/pages/session/use-session-hash-scroll.ts:149` -- External sync in: - - `packages/app/src/context/language.tsx:207` - - `packages/app/src/context/settings.tsx:110` - - `packages/app/src/context/sdk.tsx:26` -- Polling in: - - `packages/app/src/components/status-popover.tsx:59` - - `packages/app/src/components/dialog-select-server.tsx:273` - -Rules: - -- Keep the effect single-purpose -- Make dependencies explicit and narrow -- Avoid writing back into the same reactive graph unless absolutely required - -## Implementation Plan - -### Phase 0: Classification Pass - -Before changing code, tag each targeted effect as one of: derive, reset, event, lifecycle, or external bridge. - -Acceptance criteria: - -- Every targeted effect in this spec is tagged with a replacement strategy before refactoring starts -- Shared helpers to be introduced are identified up front to avoid repeating patterns - -### Phase 1: Derived-State Cleanup - -Tackle highest-value, lowest-risk derived-state cleanup first. - -Priority items: - -- Normalize tabs at write boundaries and remove `packages/app/src/pages/session.tsx:141` -- Stop syncing `workspaceOrder` in `packages/app/src/pages/layout.tsx:557` -- Make prompt slash filtering reactive so `packages/app/src/components/prompt-input.tsx:652` can be removed -- Replace other obvious derived-state effects in terminal and session header - -Acceptance criteria: - -- No behavior change in tab ordering, prompt filtering, terminal display, or header state -- Targeted derived-state effects are deleted, not just moved - -### Phase 2: Keyed Reset Cleanup - -Replace reset-on-key effects with keyed ownership boundaries. - -Priority items: - -- Key session-scoped UI and state by `sessionKey` -- Key file-scoped state by `scope()` -- Remove manual clear-and-reseed effects in session and file context - -Acceptance criteria: - -- Switching session or file scope recreates the intended local state cleanly -- No stale state leaks across session or scope changes -- Target reset effects are deleted - -### Phase 3: Event-Driven Work Extraction - -Move event-driven work out of reactive effects. - -Priority items: - -- Replace `globalStore.reload` effect dispatching with direct calls -- Split mixed-responsibility effect in `packages/app/src/pages/layout.tsx:1489` -- Collapse duplicated imperative trigger triplets into single functions -- Move file-tree and terminal-panel imperative work to explicit handlers - -Acceptance criteria: - -- User-triggered behavior still fires exactly once per intended action -- No effect remains whose only job is to notice a command-like state and trigger an imperative function - -### Phase 4: Context Ownership Cleanup - -Remove mirrored child-store hydration patterns. - -Priority items: - -- Remove child-store hydration mirrors in `packages/app/src/context/global-sync/child-store.ts:184`, `:190`, `:193` -- Simplify mirror logic in `packages/app/src/context/global-sync.tsx:130`, `:138` -- Revisit `packages/app/src/context/layout.tsx:424` if it still mirrors instead of deriving - -Acceptance criteria: - -- There is one clear source of truth for each synced value -- Child stores no longer need effect-based hydration to stay consistent -- Initialization and updates both work without manual mirror effects - -### Phase 5: Cleanup And Keeper Review - -Clean up remaining targeted hotspots and narrow the effects that should stay. - -Acceptance criteria: - -- Remaining `createEffect` calls in touched files are all true bridges or clearly justified lifecycle sync -- Mixed-responsibility effects are split into smaller units where still needed - -## Detailed Work Items By Area - -### 1. Normalize Tab State - -Files: - -- `packages/app/src/pages/session.tsx:141` - -Work: - -- Move tab normalization into the functions that create, load, or update tab state -- Make readers consume already-normalized tab data -- Remove the effect that rewrites derived tab state after the fact - -Rationale: - -- Tabs should become valid when written, not be repaired later -- This removes a feedback loop and makes state easier to trust - -Acceptance criteria: - -- The effect at `packages/app/src/pages/session.tsx:141` is removed -- Newly created and restored tabs are normalized before they enter local state -- Tab rendering still matches current behavior for valid and edge-case inputs - -### 2. Key Session-Owned State - -Files: - -- `packages/app/src/pages/session.tsx:325` -- `packages/app/src/pages/session.tsx:336` -- `packages/app/src/pages/session.tsx:477` -- `packages/app/src/pages/session.tsx:869` -- `packages/app/src/pages/session.tsx:963` -- `packages/app/src/pages/session/message-timeline.tsx:149` - -Work: - -- Identify state that should reset when `sessionKey` changes -- Move that state under a keyed subtree or keyed owner boundary -- Remove effects that watch `sessionKey` just to clear local state, refs, or temporary UI flags - -Rationale: - -- Session identity already defines the lifetime of this UI state -- Keyed ownership makes reset behavior automatic and easier to reason about - -Acceptance criteria: - -- The targeted reset effects are removed -- Changing sessions resets only the intended session-local state -- Scroll and editor state that should persist are not accidentally reset - -### 3. Derive Workspace Order - -Files: - -- `packages/app/src/pages/layout.tsx:557` - -Work: - -- Stop writing `workspaceOrder` from live workspace data in an effect -- Represent user overrides separately from live workspace data -- Compute effective order from current data plus overrides with a memo or pure helper - -Rationale: - -- Persisted user intent and live source data should not mirror each other through an effect -- A computed effective order avoids drift and racey resync behavior - -Acceptance criteria: - -- The effect at `packages/app/src/pages/layout.tsx:557` is removed -- Workspace order updates correctly when workspaces appear, disappear, or are reordered by the user -- User overrides persist without requiring a sync-back effect - -### 4. Remove Child-Store Mirrors - -Files: - -- `packages/app/src/context/global-sync.tsx:130` -- `packages/app/src/context/global-sync.tsx:138` -- `packages/app/src/context/global-sync.tsx:148` -- `packages/app/src/context/global-sync/child-store.ts:184` -- `packages/app/src/context/global-sync/child-store.ts:190` -- `packages/app/src/context/global-sync/child-store.ts:193` -- `packages/app/src/context/layout.tsx:424` - -Work: - -- Trace the actual ownership of global and child store values -- Replace hydration and mirror effects with explicit initialization and direct updates -- Remove the `globalStore.reload` event-bus pattern and call the needed reload paths directly - -Rationale: - -- Mirrors make it hard to tell which state is authoritative -- Event-bus style state toggles hide control flow and create accidental reruns - -Acceptance criteria: - -- Child store hydration no longer depends on effect-based copying -- Reload work can be followed from the event source to the handler without a reactive relay -- State remains correct on first load, child creation, and subsequent updates - -### 5. Key File-Scoped State - -Files: - -- `packages/app/src/context/file.tsx:100` - -Work: - -- Move file-scoped local state under a boundary keyed by `scope()` -- Remove any effect that watches `scope()` only to reset file-local state - -Rationale: - -- File scope changes are identity changes -- Keyed ownership gives a cleaner reset than manual clear logic - -Acceptance criteria: - -- The effect at `packages/app/src/context/file.tsx:100` is removed -- Switching scopes resets only scope-local state -- No previous-scope data appears after a scope change - -### 6. Split Layout Side Effects - -Files: - -- `packages/app/src/pages/layout.tsx:1489` -- Related event-driven effects near `packages/app/src/pages/layout.tsx:484`, `:652`, `:776`, `:1519` - -Work: - -- Break the mixed-responsibility effect at `:1489` into direct actions and smaller bridge effects only where required -- Move user-triggered branches into the actual command or handler that causes them -- Remove any branch that only exists because one effect is handling unrelated concerns - -Rationale: - -- Mixed effects hide cause and make reruns hard to predict -- Smaller units reduce accidental coupling and make future cleanup safer - -Acceptance criteria: - -- The effect at `packages/app/src/pages/layout.tsx:1489` no longer mixes unrelated responsibilities -- Event-driven branches execute from direct handlers -- Remaining effects in this area each have one clear external sync purpose - -### 7. Remove Duplicate Triggers - -Files: - -- `packages/app/src/pages/session/review-tab.tsx:122` -- `packages/app/src/pages/session/review-tab.tsx:130` -- `packages/app/src/pages/session/review-tab.tsx:138` -- `packages/app/src/pages/session/file-tabs.tsx:367` -- `packages/app/src/pages/session/file-tabs.tsx:378` -- `packages/app/src/pages/session/file-tabs.tsx:389` -- `packages/app/src/pages/session/use-session-hash-scroll.ts:144` -- `packages/app/src/pages/session/use-session-hash-scroll.ts:149` -- `packages/app/src/pages/session/use-session-hash-scroll.ts:167` - -Work: - -- Extract one explicit imperative function per behavior -- Call that function from each source event instead of replicating the same effect pattern multiple times -- Preserve the scroll-sync effect that is truly syncing with the DOM, but remove duplicate trigger scaffolding around it - -Rationale: - -- Duplicate triggers make it easy to miss a case or fire twice -- One named action is easier to test and reason about - -Acceptance criteria: - -- Repeated imperative effect triplets are collapsed into shared functions -- Scroll behavior still works, including hash-based navigation -- No duplicate firing is introduced - -### 8. Make Prompt Filtering Reactive - -Files: - -- `packages/app/src/components/prompt-input.tsx:652` -- Keep `packages/app/src/components/prompt-input.tsx:690` as needed - -Work: - -- Convert slash filtering into a pure reactive derivation from the current input and candidate command list -- Keep only the editor or DOM bridge effect if it is still needed for imperative syncing - -Rationale: - -- Filtering is classic derived state -- It should not need an effect if it can be computed from current inputs - -Acceptance criteria: - -- The effect at `packages/app/src/components/prompt-input.tsx:652` is removed -- Filtered slash-command results update correctly as the input changes -- The editor sync effect at `:690` still behaves correctly - -### 9. Clean Up Smaller Derived-State Cases - -Files: - -- `packages/app/src/components/terminal.tsx:261` -- `packages/app/src/components/session/session-header.tsx:309` - -Work: - -- Replace effect-written local state with memos or inline derivation -- Remove intermediate setters when the value can be computed directly - -Rationale: - -- These are low-risk wins that reinforce the same pattern -- They also help keep follow-up cleanup consistent - -Acceptance criteria: - -- Targeted effects are removed -- UI output remains unchanged under the same inputs - -## Verification And Regression Checks - -Run focused checks after each phase, not only at the end. - -### Suggested Verification - -- Switch between sessions rapidly and confirm local session UI resets only where intended -- Open, close, and reorder tabs and confirm order and normalization remain stable -- Change workspaces, reload workspace data, and verify effective ordering is correct -- Change file scope and confirm stale file state does not bleed across scopes -- Trigger layout actions that previously depended on effects and confirm they still fire once -- Use slash commands in the prompt and verify filtering updates as you type -- Test review tab, file tab, and hash-scroll flows for duplicate or missing triggers -- Verify global sync initialization, reload, and child-store creation paths - -### Regression Checks - -- No accidental infinite reruns -- No double-firing network or command actions -- No lost cleanup for listeners, timers, or scroll handlers -- No preserved stale state after identity changes -- No removed effect that was actually bridging to DOM or an external API - -If available, add or update tests around pure helpers introduced during this cleanup. - -Favor tests for derived ordering, normalization, and action extraction, since those are easiest to lock down. - -## Definition Of Done - -This work is done when all of the following are true: - -- The highest-leverage targets in this spec are implemented -- Each removed effect has been replaced by a clearer pattern: memo, keyed boundary, direct action, or lifecycle hook -- The "should remain" effects still exist only where they serve a real external sync purpose -- Touched files have fewer mixed-responsibility effects and clearer ownership of state -- Manual verification covers session switching, file scope changes, workspace ordering, prompt filtering, and reload flows -- No behavior regressions are found in the targeted areas - -A reduced raw `createEffect` count is helpful, but it is not the main success metric. - -The main success metric is clearer ownership and fewer effect-driven state repairs. - -## Risks And Rollout Notes - -Main risks: - -- Keyed remounts can reset too much if state boundaries are drawn too high -- Store mirror removal can break initialization order if ownership is not mapped first -- Moving event work out of effects can accidentally skip triggers that were previously implicit - -Rollout notes: - -- Land in small phases, with each phase keeping the app behaviorally stable -- Prefer isolated PRs by phase or by file cluster, especially for context-store changes -- Review each remaining effect in touched files and leave it only if it clearly bridges to something external diff --git a/packages/app/e2e/prompt/prompt-history.spec.ts b/packages/app/e2e/prompt/prompt-history.spec.ts deleted file mode 100644 index d880c5e6518..00000000000 --- a/packages/app/e2e/prompt/prompt-history.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { ToolPart } from "@kilocode/sdk/v2/client" -import type { Page } from "@playwright/test" -import { test, expect } from "../fixtures" -import { assistantText } from "../actions" -import { promptSelector } from "../selectors" -import { createSdk } from "../utils" - -const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim() -type Sdk = ReturnType - -const isBash = (part: unknown): part is ToolPart => { - if (!part || typeof part !== "object") return false - if (!("type" in part) || part.type !== "tool") return false - if (!("tool" in part) || part.tool !== "bash") return false - return "state" in part -} - -async function wait(page: Page, value: string) { - await expect.poll(async () => text(await page.locator(promptSelector).textContent())).toBe(value) -} - -async function reply(sdk: Sdk, sessionID: string, token: string) { - await expect.poll(() => assistantText(sdk, sessionID), { timeout: 90_000 }).toContain(token) -} - -async function shell(sdk: Sdk, sessionID: string, cmd: string, token: string) { - await expect - .poll( - async () => { - const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? []) - const part = messages - .filter((item) => item.info.role === "assistant") - .flatMap((item) => item.parts) - .filter(isBash) - .find((item) => item.state.input?.command === cmd && item.state.status === "completed") - - if (!part || part.state.status !== "completed") return - return typeof part.state.metadata?.output === "string" ? part.state.metadata.output : part.state.output - }, - { timeout: 90_000 }, - ) - .toContain(token) -} - -test("prompt history restores unsent draft with arrow navigation", async ({ page, project, assistant }) => { - test.setTimeout(120_000) - - const firstToken = `E2E_HISTORY_ONE_${Date.now()}` - const secondToken = `E2E_HISTORY_TWO_${Date.now()}` - const first = `Reply with exactly: ${firstToken}` - const second = `Reply with exactly: ${secondToken}` - const draft = `draft ${Date.now()}` - - await project.open() - await assistant.reply(firstToken) - const sessionID = await project.prompt(first) - await wait(page, "") - await reply(project.sdk, sessionID, firstToken) - - await assistant.reply(secondToken) - await project.prompt(second) - await wait(page, "") - await reply(project.sdk, sessionID, secondToken) - - const prompt = page.locator(promptSelector) - await prompt.click() - await page.keyboard.type(draft) - await wait(page, draft) - - await prompt.fill("") - await wait(page, "") - - await page.keyboard.press("ArrowUp") - await wait(page, second) - - await page.keyboard.press("ArrowUp") - await wait(page, first) - - await page.keyboard.press("ArrowDown") - await wait(page, second) - - await page.keyboard.press("ArrowDown") - await wait(page, "") -}) - -test.fixme("shell history stays separate from normal prompt history", async ({ page, sdk, gotoSession }) => { - test.setTimeout(120_000) - - const firstToken = `E2E_SHELL_ONE_${Date.now()}` - const secondToken = `E2E_SHELL_TWO_${Date.now()}` - const normalToken = `E2E_NORMAL_${Date.now()}` - const first = `echo ${firstToken}` - const second = `echo ${secondToken}` - const normal = `Reply with exactly: ${normalToken}` - - await gotoSession() - - const prompt = page.locator(promptSelector) - - await prompt.click() - await page.keyboard.type("!") - await page.keyboard.type(first) - await page.keyboard.press("Enter") - await wait(page, "") - - await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 }) - const sessionID = sessionIDFromUrl(page.url())! - await shell(sdk, sessionID, first, firstToken) - - await prompt.click() - await page.keyboard.type("!") - await page.keyboard.type(second) - await page.keyboard.press("Enter") - await wait(page, "") - await shell(sdk, sessionID, second, secondToken) - - await page.keyboard.press("Escape") - await wait(page, "") - - await prompt.click() - await page.keyboard.type("!") - await page.keyboard.press("ArrowUp") - await wait(page, second) - - await page.keyboard.press("ArrowUp") - await wait(page, first) - - await page.keyboard.press("ArrowDown") - await wait(page, second) - - await page.keyboard.press("ArrowDown") - await wait(page, "") - - await page.keyboard.press("Escape") - await wait(page, "") - - await prompt.click() - await page.keyboard.type(normal) - await page.keyboard.press("Enter") - await wait(page, "") - await reply(sdk, sessionID, normalToken) - - await prompt.click() - await page.keyboard.press("ArrowUp") - await wait(page, normal) -}) diff --git a/packages/app/e2e/prompt/prompt-shell.spec.ts b/packages/app/e2e/prompt/prompt-shell.spec.ts deleted file mode 100644 index 1c76eb96b5a..00000000000 --- a/packages/app/e2e/prompt/prompt-shell.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { ToolPart } from "@kilocode/sdk/v2/client" -import { test, expect } from "../fixtures" -import { closeDialog, openSettings, withSession } from "../actions" -import { promptModelSelector, promptSelector, promptVariantSelector } from "../selectors" - -const isBash = (part: unknown): part is ToolPart => { - if (!part || typeof part !== "object") return false - if (!("type" in part) || part.type !== "tool") return false - if (!("tool" in part) || part.tool !== "bash") return false - return "state" in part -} - -test("shell mode runs a command in the project directory", async ({ page, project }) => { - test.setTimeout(120_000) - - await project.open() - const cmd = process.platform === "win32" ? "dir" : "command ls" - - await withSession(project.sdk, `e2e shell ${Date.now()}`, async (session) => { - project.trackSession(session.id) - await project.gotoSession(session.id) - const dialog = await openSettings(page) - const toggle = dialog.locator('[data-action="settings-auto-accept-permissions"]').first() - const input = toggle.locator('[data-slot="switch-input"]').first() - await expect(toggle).toBeVisible() - if ((await input.getAttribute("aria-checked")) !== "true") { - await toggle.locator('[data-slot="switch-control"]').click() - await expect(input).toHaveAttribute("aria-checked", "true") - } - await closeDialog(page, dialog) - await project.shell(cmd) - - await expect - .poll( - async () => { - const list = await project.sdk.session - .messages({ sessionID: session.id, limit: 50 }) - .then((x) => x.data ?? []) - const msg = list.findLast( - (item) => item.info.role === "assistant" && "path" in item.info && item.info.path.cwd === project.directory, - ) - if (!msg) return - - const part = msg.parts - .filter(isBash) - .find((item) => item.state.input?.command === cmd && item.state.status === "completed") - - if (!part || part.state.status !== "completed") return - const output = - typeof part.state.metadata?.output === "string" ? part.state.metadata.output : part.state.output - if (!output.includes("README.md")) return - - return { cwd: project.directory, output } - }, - { timeout: 90_000 }, - ) - .toEqual(expect.objectContaining({ cwd: project.directory, output: expect.stringContaining("README.md") })) - }) -}) - -test("shell mode unmounts model and variant controls", async ({ page, project }) => { - await project.open() - - const prompt = page.locator(promptSelector).first() - await expect(page.locator(promptModelSelector)).toHaveCount(1) - await expect(page.locator(promptVariantSelector)).toHaveCount(1) - - await prompt.click() - await page.keyboard.type("!") - - await expect(prompt).toHaveAttribute("aria-label", /enter shell command/i) - await expect(page.locator(promptModelSelector)).toHaveCount(0) - await expect(page.locator(promptVariantSelector)).toHaveCount(0) -}) diff --git a/packages/app/e2e/prompt/prompt-slash-share.spec.ts b/packages/app/e2e/prompt/prompt-slash-share.spec.ts deleted file mode 100644 index f72a76fc3e4..00000000000 --- a/packages/app/e2e/prompt/prompt-slash-share.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { test, expect } from "../fixtures" -import { promptSelector } from "../selectors" -import { withSession } from "../actions" - -const shareDisabled = process.env.KILO_DISABLE_SHARE === "true" || process.env.KILO_DISABLE_SHARE === "1" - -async function seed(sdk: Parameters[0], sessionID: string) { - await sdk.session.promptAsync({ - sessionID, - noReply: true, - parts: [{ type: "text", text: "e2e share seed" }], - }) - - await expect - .poll( - async () => { - const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? []) - return messages.length - }, - { timeout: 30_000 }, - ) - .toBeGreaterThan(0) -} - -test("/share and /unshare update session share state", async ({ page, project }) => { - test.skip(shareDisabled, "Share is disabled in this environment (KILO_DISABLE_SHARE).") - - await project.open() - await withSession(project.sdk, `e2e slash share ${Date.now()}`, async (session) => { - project.trackSession(session.id) - const prompt = page.locator(promptSelector) - - await seed(project.sdk, session.id) - await project.gotoSession(session.id) - - await prompt.click() - await page.keyboard.type("/share") - await expect(page.locator('[data-slash-id="session.share"]').first()).toBeVisible() - await page.keyboard.press("Enter") - - await expect - .poll( - async () => { - const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data) - return data?.share?.url || undefined - }, - { timeout: 30_000 }, - ) - .not.toBeUndefined() - - await prompt.click() - await page.keyboard.type("/unshare") - await expect(page.locator('[data-slash-id="session.unshare"]').first()).toBeVisible() - await page.keyboard.press("Enter") - - await expect - .poll( - async () => { - const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data) - return data?.share?.url || undefined - }, - { timeout: 30_000 }, - ) - .toBeUndefined() - }) -}) diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts deleted file mode 100644 index 3c83656f88d..00000000000 --- a/packages/app/e2e/session/session.spec.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { test, expect } from "../fixtures" -import { - openSidebar, - openSessionMoreMenu, - clickMenuItem, - confirmDialog, - openSharePopover, - withSession, -} from "../actions" -import { sessionItemSelector, inlineInputSelector } from "../selectors" - -const shareDisabled = process.env.KILO_DISABLE_SHARE === "true" || process.env.KILO_DISABLE_SHARE === "1" - -type Sdk = Parameters[0] - -async function seedMessage(sdk: Sdk, sessionID: string) { - await sdk.session.promptAsync({ - sessionID, - noReply: true, - parts: [{ type: "text", text: "e2e seed" }], - }) - - await expect - .poll( - async () => { - const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? []) - return messages.length - }, - { timeout: 30_000 }, - ) - .toBeGreaterThan(0) -} - -test("session can be renamed via header menu", async ({ page, project }) => { - const stamp = Date.now() - const originalTitle = `e2e rename test ${stamp}` - const renamedTitle = `e2e renamed ${stamp}` - - await project.open() - await withSession(project.sdk, originalTitle, async (session) => { - project.trackSession(session.id) - await seedMessage(project.sdk, session.id) - await project.gotoSession(session.id) - await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(originalTitle) - - const menu = await openSessionMoreMenu(page, session.id) - await clickMenuItem(menu, /rename/i) - - const input = page.locator(".scroll-view__viewport").locator(inlineInputSelector).first() - await expect(input).toBeVisible() - await expect(input).toBeFocused() - await input.fill(renamedTitle) - await expect(input).toHaveValue(renamedTitle) - await input.press("Enter") - - await expect - .poll( - async () => { - const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data) - return data?.title - }, - { timeout: 30_000 }, - ) - .toBe(renamedTitle) - - await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(renamedTitle) - }) -}) - -test("session can be archived via header menu", async ({ page, project }) => { - const stamp = Date.now() - const title = `e2e archive test ${stamp}` - - await project.open() - await withSession(project.sdk, title, async (session) => { - project.trackSession(session.id) - await seedMessage(project.sdk, session.id) - await project.gotoSession(session.id) - const menu = await openSessionMoreMenu(page, session.id) - await clickMenuItem(menu, /archive/i) - - await expect - .poll( - async () => { - const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data) - return data?.time?.archived - }, - { timeout: 30_000 }, - ) - .not.toBeUndefined() - - await openSidebar(page) - await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0) - }) -}) - -test("session can be deleted via header menu", async ({ page, project }) => { - const stamp = Date.now() - const title = `e2e delete test ${stamp}` - - await project.open() - await withSession(project.sdk, title, async (session) => { - project.trackSession(session.id) - await seedMessage(project.sdk, session.id) - await project.gotoSession(session.id) - const menu = await openSessionMoreMenu(page, session.id) - await clickMenuItem(menu, /delete/i) - await confirmDialog(page, /delete/i) - - await expect - .poll( - async () => { - const data = await project.sdk.session - .get({ sessionID: session.id }) - .then((r) => r.data) - .catch(() => undefined) - return data?.id - }, - { timeout: 30_000 }, - ) - .toBeUndefined() - - await openSidebar(page) - await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0) - }) -}) - -test("session can be shared and unshared via header button", async ({ page, project }) => { - test.skip(shareDisabled, "Share is disabled in this environment (KILO_DISABLE_SHARE).") - - const stamp = Date.now() - const title = `e2e share test ${stamp}` - - await project.open() - await withSession(project.sdk, title, async (session) => { - project.trackSession(session.id) - await project.gotoSession(session.id) - await project.prompt(`share seed ${stamp}`) - - const shared = await openSharePopover(page) - const publish = shared.popoverBody.getByRole("button", { name: "Publish" }).first() - await expect(publish).toBeVisible({ timeout: 30_000 }) - await publish.click() - - await expect(shared.popoverBody.getByRole("button", { name: "Unpublish" }).first()).toBeVisible({ - timeout: 30_000, - }) - - await expect - .poll( - async () => { - const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data) - return data?.share?.url || undefined - }, - { timeout: 30_000 }, - ) - .not.toBeUndefined() - - const unpublish = shared.popoverBody.getByRole("button", { name: "Unpublish" }).first() - await expect(unpublish).toBeVisible({ timeout: 30_000 }) - await unpublish.click() - - await expect(shared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({ - timeout: 30_000, - }) - - await expect - .poll( - async () => { - const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data) - return data?.share?.url || undefined - }, - { timeout: 30_000 }, - ) - .toBeUndefined() - - const unshared = await openSharePopover(page) - await expect(unshared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({ - timeout: 30_000, - }) - }) -}) diff --git a/packages/app/e2e/todo.spec.ts b/packages/app/e2e/todo.spec.ts deleted file mode 100644 index dac2d8ee824..00000000000 --- a/packages/app/e2e/todo.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { test } from "@playwright/test" - -test( - "test something cool", - { - annotation: { type: "todo" }, - }, - async () => { - test.fixme() - }, -) diff --git a/packages/app/e2e/tsconfig.json b/packages/app/e2e/tsconfig.json deleted file mode 100644 index 3f1cad80cb6..00000000000 --- a/packages/app/e2e/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "noEmit": true, - "rootDir": "..", - "types": ["node", "bun"] - }, - "include": ["./**/*.ts"] -} diff --git a/packages/app/happydom.ts b/packages/app/happydom.ts deleted file mode 100644 index de726718f6f..00000000000 --- a/packages/app/happydom.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { GlobalRegistrator } from "@happy-dom/global-registrator" - -GlobalRegistrator.register() - -const originalGetContext = HTMLCanvasElement.prototype.getContext -// @ts-expect-error - we're overriding with a simplified mock -HTMLCanvasElement.prototype.getContext = function (contextType: string, _options?: unknown) { - if (contextType === "2d") { - return { - canvas: this, - fillStyle: "#000000", - strokeStyle: "#000000", - font: "12px monospace", - textAlign: "start", - textBaseline: "alphabetic", - globalAlpha: 1, - globalCompositeOperation: "source-over", - imageSmoothingEnabled: true, - lineWidth: 1, - lineCap: "butt", - lineJoin: "miter", - miterLimit: 10, - shadowBlur: 0, - shadowColor: "rgba(0, 0, 0, 0)", - shadowOffsetX: 0, - shadowOffsetY: 0, - fillRect: () => {}, - strokeRect: () => {}, - clearRect: () => {}, - fillText: () => {}, - strokeText: () => {}, - measureText: (text: string) => ({ width: text.length * 8 }), - drawImage: () => {}, - save: () => {}, - restore: () => {}, - scale: () => {}, - rotate: () => {}, - translate: () => {}, - transform: () => {}, - setTransform: () => {}, - resetTransform: () => {}, - createLinearGradient: () => ({ addColorStop: () => {} }), - createRadialGradient: () => ({ addColorStop: () => {} }), - createPattern: () => null, - beginPath: () => {}, - closePath: () => {}, - moveTo: () => {}, - lineTo: () => {}, - bezierCurveTo: () => {}, - quadraticCurveTo: () => {}, - arc: () => {}, - arcTo: () => {}, - ellipse: () => {}, - rect: () => {}, - fill: () => {}, - stroke: () => {}, - clip: () => {}, - isPointInPath: () => false, - isPointInStroke: () => false, - getTransform: () => ({}), - getImageData: () => ({ - data: new Uint8ClampedArray(0), - width: 0, - height: 0, - }), - putImageData: () => {}, - createImageData: () => ({ - data: new Uint8ClampedArray(0), - width: 0, - height: 0, - }), - } as unknown as CanvasRenderingContext2D - } - return originalGetContext.call(this, contextType as "2d", _options) -} diff --git a/packages/app/index.html b/packages/app/index.html deleted file mode 100644 index 8fad7efb3a4..00000000000 --- a/packages/app/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - OpenCode - - - - - - - - - - - - - -
- - - diff --git a/packages/app/package.json b/packages/app/package.json deleted file mode 100644 index 3f0432a63c1..00000000000 --- a/packages/app/package.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "name": "@opencode-ai/app", - "version": "7.2.33", - "description": "", - "type": "module", - "exports": { - ".": "./src/index.ts", - "./vite": "./vite.js", - "./index.css": "./src/index.css" - }, - "scripts": { - "typecheck": "tsgo -b", - "start": "vite", - "dev": "vite", - "build": "vite build", - "serve": "vite preview", - "test": "bun run test:unit", - "test:ci": "mkdir -p .artifacts/unit && bun test --preload ./happydom.ts ./src --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", - "test:unit": "bun test --preload ./happydom.ts ./src", - "test:unit:watch": "bun test --watch --preload ./happydom.ts ./src", - "test:e2e": "playwright test", - "test:e2e:local": "playwright test", - "test:e2e:ui": "playwright test --ui", - "test:e2e:report": "playwright show-report e2e/playwright-report" - }, - "license": "MIT", - "devDependencies": { - "@happy-dom/global-registrator": "20.0.11", - "@playwright/test": "catalog:", - "@tailwindcss/vite": "catalog:", - "@tsconfig/bun": "1.0.9", - "@types/bun": "catalog:", - "@types/luxon": "catalog:", - "@types/node": "catalog:", - "@typescript/native-preview": "catalog:", - "typescript": "catalog:", - "vite": "catalog:", - "vite-plugin-icons-spritesheet": "3.0.1", - "vite-plugin-solid": "catalog:" - }, - "dependencies": { - "@kobalte/core": "catalog:", - "@kilocode/sdk": "workspace:*", - "@opencode-ai/ui": "workspace:*", - "@opencode-ai/shared": "workspace:*", - "@shikijs/transformers": "3.9.2", - "@solid-primitives/active-element": "2.1.3", - "@solid-primitives/audio": "1.4.2", - "@solid-primitives/event-bus": "1.1.2", - "@solid-primitives/event-listener": "2.4.5", - "@solid-primitives/i18n": "2.2.1", - "@solid-primitives/media": "2.3.3", - "@solid-primitives/resize-observer": "2.1.5", - "@solid-primitives/scroll": "2.1.3", - "@solid-primitives/storage": "catalog:", - "@solid-primitives/timer": "1.4.4", - "@solid-primitives/websocket": "1.3.1", - "@solidjs/meta": "catalog:", - "@solidjs/router": "catalog:", - "@tanstack/solid-query": "5.91.4", - "@thisbeyond/solid-dnd": "0.7.5", - "diff": "catalog:", - "effect": "catalog:", - "fuzzysort": "catalog:", - "ghostty-web": "github:anomalyco/ghostty-web#main", - "luxon": "catalog:", - "marked": "catalog:", - "marked-shiki": "catalog:", - "remeda": "catalog:", - "shiki": "catalog:", - "solid-js": "catalog:", - "solid-list": "catalog:", - "tailwindcss": "catalog:", - "virtua": "catalog:", - "zod": "catalog:", - "@kilocode/kilo-ui": "workspace:*", - "@kilocode/kilo-i18n": "workspace:*" - }, - "peerDependencies": {} -} diff --git a/packages/app/playwright.config.ts b/packages/app/playwright.config.ts deleted file mode 100644 index 0a1e56bb4fa..00000000000 --- a/packages/app/playwright.config.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { defineConfig, devices } from "@playwright/test" - -const port = Number(process.env.PLAYWRIGHT_PORT ?? 3000) -const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${port}` -const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1" -const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" -const command = `bun run dev -- --host 0.0.0.0 --port ${port}` -const reuse = !process.env.CI -const workers = Number(process.env.PLAYWRIGHT_WORKERS ?? (process.env.CI ? 5 : 0)) || undefined -const reporter = [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]] as const - -if (process.env.PLAYWRIGHT_JUNIT_OUTPUT) { - reporter.push(["junit", { outputFile: process.env.PLAYWRIGHT_JUNIT_OUTPUT }]) -} - -export default defineConfig({ - testDir: "./e2e", - outputDir: "./e2e/test-results", - timeout: 60_000, - expect: { - timeout: 10_000, - }, - fullyParallel: process.env.PLAYWRIGHT_FULLY_PARALLEL === "1", - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers, - reporter, - webServer: { - command, - url: baseURL, - reuseExistingServer: reuse, - timeout: 120_000, - env: { - VITE_KILO_SERVER_HOST: serverHost, - VITE_KILO_SERVER_PORT: serverPort, - }, - }, - use: { - baseURL, - trace: "on-first-retry", - screenshot: "only-on-failure", - video: "retain-on-failure", - }, - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - ], -}) diff --git a/packages/app/public/_headers b/packages/app/public/_headers deleted file mode 100644 index f5157b1debc..00000000000 --- a/packages/app/public/_headers +++ /dev/null @@ -1,17 +0,0 @@ -/assets/*.js - Content-Type: application/javascript - -/assets/*.mjs - Content-Type: application/javascript - -/assets/*.css - Content-Type: text/css - -/*.js - Content-Type: application/javascript - -/*.mjs - Content-Type: application/javascript - -/*.css - Content-Type: text/css diff --git a/packages/app/public/apple-touch-icon-v3.png b/packages/app/public/apple-touch-icon-v3.png deleted file mode 120000 index a6f48a689db..00000000000 --- a/packages/app/public/apple-touch-icon-v3.png +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/apple-touch-icon-v3.png \ No newline at end of file diff --git a/packages/app/public/apple-touch-icon.png b/packages/app/public/apple-touch-icon.png deleted file mode 120000 index fb6e8b1702d..00000000000 --- a/packages/app/public/apple-touch-icon.png +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/apple-touch-icon.png \ No newline at end of file diff --git a/packages/app/public/assets/JetBrainsMonoNerdFontMono-Regular.woff2 b/packages/app/public/assets/JetBrainsMonoNerdFontMono-Regular.woff2 deleted file mode 100644 index 02a57c6f500..00000000000 Binary files a/packages/app/public/assets/JetBrainsMonoNerdFontMono-Regular.woff2 and /dev/null differ diff --git a/packages/app/public/favicon-96x96-v3.png b/packages/app/public/favicon-96x96-v3.png deleted file mode 120000 index 5d21163ce86..00000000000 --- a/packages/app/public/favicon-96x96-v3.png +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/favicon-96x96-v3.png \ No newline at end of file diff --git a/packages/app/public/favicon-96x96.png b/packages/app/public/favicon-96x96.png deleted file mode 120000 index 155c5ed2fc1..00000000000 --- a/packages/app/public/favicon-96x96.png +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/favicon-96x96.png \ No newline at end of file diff --git a/packages/app/public/favicon-v3.ico b/packages/app/public/favicon-v3.ico deleted file mode 120000 index b3da91f3c45..00000000000 --- a/packages/app/public/favicon-v3.ico +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/favicon-v3.ico \ No newline at end of file diff --git a/packages/app/public/favicon-v3.svg b/packages/app/public/favicon-v3.svg deleted file mode 120000 index fc95f68af4a..00000000000 --- a/packages/app/public/favicon-v3.svg +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/favicon-v3.svg \ No newline at end of file diff --git a/packages/app/public/favicon.ico b/packages/app/public/favicon.ico deleted file mode 120000 index 1c90f01b16f..00000000000 --- a/packages/app/public/favicon.ico +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/favicon.ico \ No newline at end of file diff --git a/packages/app/public/favicon.svg b/packages/app/public/favicon.svg deleted file mode 120000 index 80804d2579b..00000000000 --- a/packages/app/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/favicon.svg \ No newline at end of file diff --git a/packages/app/public/oc-theme-preload.js b/packages/app/public/oc-theme-preload.js deleted file mode 100644 index 36fa5d726af..00000000000 --- a/packages/app/public/oc-theme-preload.js +++ /dev/null @@ -1,35 +0,0 @@ -;(function () { - var key = "opencode-theme-id" - var themeId = localStorage.getItem(key) || "oc-2" - - if (themeId === "oc-1") { - themeId = "oc-2" - localStorage.setItem(key, themeId) - localStorage.removeItem("opencode-theme-css-light") - localStorage.removeItem("opencode-theme-css-dark") - } - - var scheme = localStorage.getItem("opencode-color-scheme") || "system" - var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches) - var mode = isDark ? "dark" : "light" - - document.documentElement.dataset.theme = themeId - document.documentElement.dataset.colorScheme = mode - - if (themeId === "oc-2") return - - var css = localStorage.getItem("opencode-theme-css-" + mode) - if (css) { - var style = document.createElement("style") - style.id = "oc-theme-preload" - style.textContent = - ":root{color-scheme:" + - mode + - ";--text-mix-blend-mode:" + - (isDark ? "plus-lighter" : "multiply") + - ";" + - css + - "}" - document.head.appendChild(style) - } -})() diff --git a/packages/app/public/site.webmanifest b/packages/app/public/site.webmanifest deleted file mode 120000 index a116d787962..00000000000 --- a/packages/app/public/site.webmanifest +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/site.webmanifest \ No newline at end of file diff --git a/packages/app/public/social-share-zen.png b/packages/app/public/social-share-zen.png deleted file mode 120000 index 02f205fc523..00000000000 --- a/packages/app/public/social-share-zen.png +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/images/social-share-zen.png \ No newline at end of file diff --git a/packages/app/public/social-share.png b/packages/app/public/social-share.png deleted file mode 120000 index 88bf2d4c654..00000000000 --- a/packages/app/public/social-share.png +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/images/social-share.png \ No newline at end of file diff --git a/packages/app/public/web-app-manifest-192x192.png b/packages/app/public/web-app-manifest-192x192.png deleted file mode 120000 index 8cfdf8ca55f..00000000000 --- a/packages/app/public/web-app-manifest-192x192.png +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/web-app-manifest-192x192.png \ No newline at end of file diff --git a/packages/app/public/web-app-manifest-512x512.png b/packages/app/public/web-app-manifest-512x512.png deleted file mode 120000 index 4165998e654..00000000000 --- a/packages/app/public/web-app-manifest-512x512.png +++ /dev/null @@ -1 +0,0 @@ -../../ui/src/assets/favicon/web-app-manifest-512x512.png \ No newline at end of file diff --git a/packages/app/src/addons/serialize.test.ts b/packages/app/src/addons/serialize.test.ts deleted file mode 100644 index 6828e60f847..00000000000 --- a/packages/app/src/addons/serialize.test.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { describe, test, expect, beforeAll, afterEach } from "bun:test" -import { Terminal, Ghostty } from "ghostty-web" -import { SerializeAddon } from "./serialize" - -let ghostty: Ghostty -beforeAll(async () => { - ghostty = await Ghostty.load() -}) - -const terminals: Terminal[] = [] - -afterEach(() => { - for (const term of terminals) { - term.dispose() - } - terminals.length = 0 - document.body.innerHTML = "" -}) - -function createTerminal(cols = 80, rows = 24): { term: Terminal; addon: SerializeAddon; container: HTMLElement } { - const container = document.createElement("div") - document.body.appendChild(container) - - const term = new Terminal({ cols, rows, ghostty }) - const addon = new SerializeAddon() - term.loadAddon(addon) - term.open(container) - terminals.push(term) - - return { term, addon, container } -} - -function writeAndWait(term: Terminal, data: string): Promise { - return new Promise((resolve) => { - term.write(data, resolve) - }) -} - -describe("SerializeAddon", () => { - describe("ANSI color preservation", () => { - test("should preserve text attributes (bold, italic, underline)", async () => { - const { term, addon } = createTerminal() - - const input = "\x1b[1mBOLD\x1b[0m \x1b[3mITALIC\x1b[0m \x1b[4mUNDER\x1b[0m" - await writeAndWait(term, input) - - const origLine = term.buffer.active.getLine(0) - expect(origLine!.getCell(0)!.isBold()).toBe(1) - expect(origLine!.getCell(5)!.isItalic()).toBe(1) - expect(origLine!.getCell(12)!.isUnderline()).toBe(1) - - const serialized = addon.serialize({ range: { start: 0, end: 0 } }) - - const { term: term2 } = createTerminal() - terminals.push(term2) - await writeAndWait(term2, serialized) - - const line = term2.buffer.active.getLine(0) - - const boldCell = line!.getCell(0) - expect(boldCell!.getChars()).toBe("B") - expect(boldCell!.isBold()).toBe(1) - - const italicCell = line!.getCell(5) - expect(italicCell!.getChars()).toBe("I") - expect(italicCell!.isItalic()).toBe(1) - - const underCell = line!.getCell(12) - expect(underCell!.getChars()).toBe("U") - expect(underCell!.isUnderline()).toBe(1) - }) - - test("should preserve basic 16-color foreground colors", async () => { - const { term, addon } = createTerminal() - - const input = "\x1b[31mRED\x1b[32mGREEN\x1b[34mBLUE\x1b[0mNORMAL" - await writeAndWait(term, input) - - const origLine = term.buffer.active.getLine(0) - const origRedFg = origLine!.getCell(0)!.getFgColor() - const origGreenFg = origLine!.getCell(3)!.getFgColor() - const origBlueFg = origLine!.getCell(8)!.getFgColor() - - const serialized = addon.serialize({ range: { start: 0, end: 0 } }) - - const { term: term2 } = createTerminal() - terminals.push(term2) - await writeAndWait(term2, serialized) - - const line = term2.buffer.active.getLine(0) - expect(line).toBeDefined() - - const redCell = line!.getCell(0) - expect(redCell!.getChars()).toBe("R") - expect(redCell!.getFgColor()).toBe(origRedFg) - - const greenCell = line!.getCell(3) - expect(greenCell!.getChars()).toBe("G") - expect(greenCell!.getFgColor()).toBe(origGreenFg) - - const blueCell = line!.getCell(8) - expect(blueCell!.getChars()).toBe("B") - expect(blueCell!.getFgColor()).toBe(origBlueFg) - }) - - test("should preserve 256-color palette colors", async () => { - const { term, addon } = createTerminal() - - const input = "\x1b[38;5;196mRED256\x1b[0mNORMAL" - await writeAndWait(term, input) - - const origLine = term.buffer.active.getLine(0) - const origRedFg = origLine!.getCell(0)!.getFgColor() - - const serialized = addon.serialize({ range: { start: 0, end: 0 } }) - - const { term: term2 } = createTerminal() - terminals.push(term2) - await writeAndWait(term2, serialized) - - const line = term2.buffer.active.getLine(0) - const redCell = line!.getCell(0) - expect(redCell!.getChars()).toBe("R") - expect(redCell!.getFgColor()).toBe(origRedFg) - }) - - test("should preserve RGB/truecolor colors", async () => { - const { term, addon } = createTerminal() - - const input = "\x1b[38;2;255;128;64mRGB_TEXT\x1b[0mNORMAL" - await writeAndWait(term, input) - - const origLine = term.buffer.active.getLine(0) - const origRgbFg = origLine!.getCell(0)!.getFgColor() - - const serialized = addon.serialize({ range: { start: 0, end: 0 } }) - - const { term: term2 } = createTerminal() - terminals.push(term2) - await writeAndWait(term2, serialized) - - const line = term2.buffer.active.getLine(0) - const rgbCell = line!.getCell(0) - expect(rgbCell!.getChars()).toBe("R") - expect(rgbCell!.getFgColor()).toBe(origRgbFg) - }) - - test("should preserve background colors", async () => { - const { term, addon } = createTerminal() - - const input = "\x1b[48;2;255;0;0mRED_BG\x1b[48;2;0;255;0mGREEN_BG\x1b[0mNORMAL" - await writeAndWait(term, input) - - const origLine = term.buffer.active.getLine(0) - const origRedBg = origLine!.getCell(0)!.getBgColor() - const origGreenBg = origLine!.getCell(6)!.getBgColor() - - const serialized = addon.serialize({ range: { start: 0, end: 0 } }) - - const { term: term2 } = createTerminal() - terminals.push(term2) - await writeAndWait(term2, serialized) - - const line = term2.buffer.active.getLine(0) - - const redBgCell = line!.getCell(0) - expect(redBgCell!.getChars()).toBe("R") - expect(redBgCell!.getBgColor()).toBe(origRedBg) - - const greenBgCell = line!.getCell(6) - expect(greenBgCell!.getChars()).toBe("G") - expect(greenBgCell!.getBgColor()).toBe(origGreenBg) - }) - - test("should handle combined colors and attributes", async () => { - const { term, addon } = createTerminal() - - const input = - "\x1b[1;38;2;255;0;0;48;2;255;255;0mCOMBO\x1b[0mNORMAL " - await writeAndWait(term, input) - - const origLine = term.buffer.active.getLine(0) - const _origFg = origLine!.getCell(0)!.getFgColor() - const _origBg = origLine!.getCell(0)!.getBgColor() - expect(origLine!.getCell(0)!.isBold()).toBe(1) - - const serialized = addon.serialize({ range: { start: 0, end: 0 } }) - const cleanSerialized = serialized.replace(/\x1b\[\d+X/g, "") - - expect(cleanSerialized.startsWith("\x1b[1;")).toBe(true) - - const { term: term2 } = createTerminal() - terminals.push(term2) - await writeAndWait(term2, cleanSerialized) - - const line = term2.buffer.active.getLine(0) - const comboCell = line!.getCell(0) - - expect(comboCell!.getChars()).toBe("C") - expect(cleanSerialized).toContain("\x1b[1;38;2;255;0;0;48;2;255;255;0m") - }) - }) - - describe("round-trip serialization", () => { - test("should not produce ECH sequences", async () => { - const { term, addon } = createTerminal() - - await writeAndWait(term, "\x1b[31mHello\x1b[0m World") - - const serialized = addon.serialize() - - const hasECH = /\x1b\[\d+X/.test(serialized) - expect(hasECH).toBe(false) - }) - - test("multi-line content should not have garbage characters", async () => { - const { term, addon } = createTerminal() - - const content = [ - "\x1b[1;32m❯\x1b[0m \x1b[34mcd\x1b[0m /some/path", - "\x1b[1;32m❯\x1b[0m \x1b[34mls\x1b[0m -la", - "total 42", - ].join("\r\n") - - await writeAndWait(term, content) - - const serialized = addon.serialize() - - expect(/\x1b\[\d+X/.test(serialized)).toBe(false) - - const { term: term2 } = createTerminal() - terminals.push(term2) - await writeAndWait(term2, serialized) - - for (let row = 0; row < 3; row++) { - const line = term2.buffer.active.getLine(row)?.translateToString(true) - expect(line?.includes("𑼝")).toBe(false) - } - - expect(term2.buffer.active.getLine(0)?.translateToString(true)).toContain("cd /some/path") - expect(term2.buffer.active.getLine(1)?.translateToString(true)).toContain("ls -la") - expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42") - }) - - test("serialized output should restore after Terminal.reset()", async () => { - const { term, addon } = createTerminal() - - const content = [ - "\x1b[1;32m❯\x1b[0m \x1b[34mcd\x1b[0m /some/path", - "\x1b[1;32m❯\x1b[0m \x1b[34mls\x1b[0m -la", - "total 42", - ].join("\r\n") - - await writeAndWait(term, content) - - const serialized = addon.serialize() - - const { term: term2 } = createTerminal() - terminals.push(term2) - term2.reset() - await writeAndWait(term2, serialized) - - expect(term2.buffer.active.getLine(0)?.translateToString(true)).toContain("cd /some/path") - expect(term2.buffer.active.getLine(1)?.translateToString(true)).toContain("ls -la") - expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42") - }) - - test("alternate buffer should round-trip without garbage", async () => { - const { term, addon } = createTerminal(20, 5) - - await writeAndWait(term, "normal\r\n") - await writeAndWait(term, "\x1b[?1049h\x1b[HALT") - - expect(term.buffer.active.type).toBe("alternate") - - const serialized = addon.serialize() - - const { term: term2 } = createTerminal(20, 5) - terminals.push(term2) - await writeAndWait(term2, serialized) - - expect(term2.buffer.active.type).toBe("alternate") - - const line = term2.buffer.active.getLine(0) - expect(line?.translateToString(true)).toBe("ALT") - - // Ensure a cell beyond content isn't garbage - const cellCode = line?.getCell(10)?.getCode() - expect(cellCode === 0 || cellCode === 32).toBe(true) - }) - - test("serialized output written to new terminal should match original colors", async () => { - const { term, addon } = createTerminal(40, 5) - - const input = "\x1b[38;2;255;0;0mHello\x1b[0m \x1b[38;2;0;255;0mWorld\x1b[0m! " - await writeAndWait(term, input) - - const origLine = term.buffer.active.getLine(0) - const origHelloFg = origLine!.getCell(0)!.getFgColor() - const origWorldFg = origLine!.getCell(6)!.getFgColor() - - const serialized = addon.serialize({ range: { start: 0, end: 0 } }) - - const { term: term2 } = createTerminal(40, 5) - terminals.push(term2) - await writeAndWait(term2, serialized) - - const newLine = term2.buffer.active.getLine(0) - - expect(newLine!.getCell(0)!.getChars()).toBe("H") - expect(newLine!.getCell(0)!.getFgColor()).toBe(origHelloFg) - - expect(newLine!.getCell(6)!.getChars()).toBe("W") - expect(newLine!.getCell(6)!.getFgColor()).toBe(origWorldFg) - - expect(newLine!.getCell(11)!.getChars()).toBe("!") - }) - }) -}) diff --git a/packages/app/src/addons/serialize.ts b/packages/app/src/addons/serialize.ts deleted file mode 100644 index 3823fb443af..00000000000 --- a/packages/app/src/addons/serialize.ts +++ /dev/null @@ -1,634 +0,0 @@ -/** - * SerializeAddon - Serialize terminal buffer contents - * - * Port of xterm.js addon-serialize for ghostty-web. - * Enables serialization of terminal contents to a string that can - * be written back to restore terminal state. - * - * Usage: - * ```typescript - * const serializeAddon = new SerializeAddon(); - * term.loadAddon(serializeAddon); - * const content = serializeAddon.serialize(); - * ``` - */ - -import type { ITerminalAddon, ITerminalCore, IBufferRange } from "ghostty-web" - -// ============================================================================ -// Buffer Types (matching ghostty-web internal interfaces) -// ============================================================================ - -interface IBuffer { - readonly type: "normal" | "alternate" - readonly cursorX: number - readonly cursorY: number - readonly viewportY: number - readonly baseY: number - readonly length: number - getLine(y: number): IBufferLine | undefined - getNullCell(): IBufferCell -} - -interface IBufferLine { - readonly length: number - readonly isWrapped: boolean - getCell(x: number): IBufferCell | undefined - translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string -} - -interface IBufferCell { - getChars(): string - getCode(): number - getWidth(): number - getFgColorMode(): number - getBgColorMode(): number - getFgColor(): number - getBgColor(): number - isBold(): number - isItalic(): number - isUnderline(): number - isStrikethrough(): number - isBlink(): number - isInverse(): number - isInvisible(): number - isFaint(): number - isDim(): boolean -} - -type TerminalBuffers = { - active?: IBuffer - normal?: IBuffer - alternate?: IBuffer -} - -const isRecord = (value: unknown): value is Record => { - return typeof value === "object" && value !== null -} - -const isBuffer = (value: unknown): value is IBuffer => { - if (!isRecord(value)) return false - if (typeof value.length !== "number") return false - if (typeof value.cursorX !== "number") return false - if (typeof value.cursorY !== "number") return false - if (typeof value.baseY !== "number") return false - if (typeof value.viewportY !== "number") return false - if (typeof value.getLine !== "function") return false - if (typeof value.getNullCell !== "function") return false - return true -} - -const getTerminalBuffers = (value: ITerminalCore): TerminalBuffers | undefined => { - if (!isRecord(value)) return - const raw = value.buffer - if (!isRecord(raw)) return - const active = isBuffer(raw.active) ? raw.active : undefined - const normal = isBuffer(raw.normal) ? raw.normal : undefined - const alternate = isBuffer(raw.alternate) ? raw.alternate : undefined - if (!active && !normal) return - return { active, normal, alternate } -} - -// ============================================================================ -// Types -// ============================================================================ - -export interface ISerializeOptions { - /** - * The row range to serialize. When an explicit range is specified, the cursor - * will get its final repositioning. - */ - range?: ISerializeRange - /** - * The number of rows in the scrollback buffer to serialize, starting from - * the bottom of the scrollback buffer. When not specified, all available - * rows in the scrollback buffer will be serialized. - */ - scrollback?: number - /** - * Whether to exclude the terminal modes from the serialization. - * Default: false - */ - excludeModes?: boolean - /** - * Whether to exclude the alt buffer from the serialization. - * Default: false - */ - excludeAltBuffer?: boolean -} - -export interface ISerializeRange { - /** - * The line to start serializing (inclusive). - */ - start: number - /** - * The line to end serializing (inclusive). - */ - end: number -} - -export interface IHTMLSerializeOptions { - /** - * The number of rows in the scrollback buffer to serialize, starting from - * the bottom of the scrollback buffer. - */ - scrollback?: number - /** - * Whether to only serialize the selection. - * Default: false - */ - onlySelection?: boolean - /** - * Whether to include the global background of the terminal. - * Default: false - */ - includeGlobalBackground?: boolean - /** - * The range to serialize. This is prioritized over onlySelection. - */ - range?: { - startLine: number - endLine: number - startCol: number - } -} - -// ============================================================================ -// Helper Functions -// ============================================================================ - -function constrain(value: number, low: number, high: number): number { - return Math.max(low, Math.min(value, high)) -} - -function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean { - return cell1.getFgColorMode() === cell2.getFgColorMode() && cell1.getFgColor() === cell2.getFgColor() -} - -function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean { - return cell1.getBgColorMode() === cell2.getBgColorMode() && cell1.getBgColor() === cell2.getBgColor() -} - -function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean { - return ( - !!cell1.isInverse() === !!cell2.isInverse() && - !!cell1.isBold() === !!cell2.isBold() && - !!cell1.isUnderline() === !!cell2.isUnderline() && - !!cell1.isBlink() === !!cell2.isBlink() && - !!cell1.isInvisible() === !!cell2.isInvisible() && - !!cell1.isItalic() === !!cell2.isItalic() && - !!cell1.isDim() === !!cell2.isDim() && - !!cell1.isStrikethrough() === !!cell2.isStrikethrough() - ) -} - -// ============================================================================ -// Base Serialize Handler -// ============================================================================ - -abstract class BaseSerializeHandler { - constructor(protected readonly _buffer: IBuffer) {} - - public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string { - let oldCell = this._buffer.getNullCell() - - const startRow = range.start.y - const endRow = range.end.y - const startColumn = range.start.x - const endColumn = range.end.x - - this._beforeSerialize(endRow - startRow + 1, startRow, endRow) - - for (let row = startRow; row <= endRow; row++) { - const line = this._buffer.getLine(row) - if (line) { - const startLineColumn = row === range.start.y ? startColumn : 0 - const endLineColumn = Math.min(endColumn, line.length) - - for (let col = startLineColumn; col < endLineColumn; col++) { - const c = line.getCell(col) - if (!c) { - continue - } - this._nextCell(c, oldCell, row, col) - oldCell = c - } - } - this._rowEnd(row, row === endRow) - } - - this._afterSerialize() - - return this._serializeString(excludeFinalCursorPosition) - } - - protected _nextCell(_cell: IBufferCell, _oldCell: IBufferCell, _row: number, _col: number): void {} - protected _rowEnd(_row: number, _isLastRow: boolean): void {} - protected _beforeSerialize(_rows: number, _startRow: number, _endRow: number): void {} - protected _afterSerialize(): void {} - protected _serializeString(_excludeFinalCursorPosition?: boolean): string { - return "" - } -} - -// ============================================================================ -// String Serialize Handler -// ============================================================================ - -class StringSerializeHandler extends BaseSerializeHandler { - private _rowIndex: number = 0 - private _allRows: string[] = [] - private _allRowSeparators: string[] = [] - private _currentRow: string = "" - private _nullCellCount: number = 0 - private _cursorStyle: IBufferCell - private _firstRow: number = 0 - private _lastCursorRow: number = 0 - private _lastCursorCol: number = 0 - private _lastContentCursorRow: number = 0 - private _lastContentCursorCol: number = 0 - - constructor( - buffer: IBuffer, - private readonly _terminal: ITerminalCore, - ) { - super(buffer) - this._cursorStyle = this._buffer.getNullCell() - } - - protected _beforeSerialize(rows: number, start: number, _end: number): void { - this._allRows = Array.from({ length: rows }) - this._allRowSeparators = Array.from({ length: rows }) - this._rowIndex = 0 - - this._currentRow = "" - this._nullCellCount = 0 - this._cursorStyle = this._buffer.getNullCell() - - this._lastContentCursorRow = start - this._lastCursorRow = start - this._firstRow = start - } - - protected _rowEnd(row: number, isLastRow: boolean): void { - let rowSeparator = "" - - const nextLine = isLastRow ? undefined : this._buffer.getLine(row + 1) - const wrapped = !!nextLine?.isWrapped - - if (this._nullCellCount > 0 && wrapped) { - this._currentRow += " ".repeat(this._nullCellCount) - } - - this._nullCellCount = 0 - - if (!isLastRow && !wrapped) { - rowSeparator = "\r\n" - this._lastCursorRow = row + 1 - this._lastCursorCol = 0 - } - - this._allRows[this._rowIndex] = this._currentRow - this._allRowSeparators[this._rowIndex++] = rowSeparator - this._currentRow = "" - this._nullCellCount = 0 - } - - private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] { - const sgrSeq: number[] = [] - const fgChanged = !equalFg(cell, oldCell) - const bgChanged = !equalBg(cell, oldCell) - const flagsChanged = !equalFlags(cell, oldCell) - - if (fgChanged || bgChanged || flagsChanged) { - if (this._isAttributeDefault(cell)) { - if (!this._isAttributeDefault(oldCell)) { - sgrSeq.push(0) - } - } else { - if (flagsChanged) { - if (!!cell.isInverse() !== !!oldCell.isInverse()) { - sgrSeq.push(cell.isInverse() ? 7 : 27) - } - if (!!cell.isBold() !== !!oldCell.isBold()) { - sgrSeq.push(cell.isBold() ? 1 : 22) - } - if (!!cell.isUnderline() !== !!oldCell.isUnderline()) { - sgrSeq.push(cell.isUnderline() ? 4 : 24) - } - if (!!cell.isBlink() !== !!oldCell.isBlink()) { - sgrSeq.push(cell.isBlink() ? 5 : 25) - } - if (!!cell.isInvisible() !== !!oldCell.isInvisible()) { - sgrSeq.push(cell.isInvisible() ? 8 : 28) - } - if (!!cell.isItalic() !== !!oldCell.isItalic()) { - sgrSeq.push(cell.isItalic() ? 3 : 23) - } - if (!!cell.isDim() !== !!oldCell.isDim()) { - sgrSeq.push(cell.isDim() ? 2 : 22) - } - if (!!cell.isStrikethrough() !== !!oldCell.isStrikethrough()) { - sgrSeq.push(cell.isStrikethrough() ? 9 : 29) - } - } - if (fgChanged) { - const color = cell.getFgColor() - const mode = cell.getFgColorMode() - if (mode === 2 || mode === 3 || mode === -1) { - sgrSeq.push(38, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff) - } else if (mode === 1) { - // Palette - if (color >= 16) { - sgrSeq.push(38, 5, color) - } else { - sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)) - } - } else { - sgrSeq.push(39) - } - } - if (bgChanged) { - const color = cell.getBgColor() - const mode = cell.getBgColorMode() - if (mode === 2 || mode === 3 || mode === -1) { - sgrSeq.push(48, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff) - } else if (mode === 1) { - // Palette - if (color >= 16) { - sgrSeq.push(48, 5, color) - } else { - sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)) - } - } else { - sgrSeq.push(49) - } - } - } - } - - return sgrSeq - } - - private _isAttributeDefault(cell: IBufferCell): boolean { - const mode = cell.getFgColorMode() - const bgMode = cell.getBgColorMode() - - if (mode === 0 && bgMode === 0) { - return ( - !cell.isBold() && - !cell.isItalic() && - !cell.isUnderline() && - !cell.isBlink() && - !cell.isInverse() && - !cell.isInvisible() && - !cell.isDim() && - !cell.isStrikethrough() - ) - } - - const fgColor = cell.getFgColor() - const bgColor = cell.getBgColor() - const nullCell = this._buffer.getNullCell() - const nullFg = nullCell.getFgColor() - const nullBg = nullCell.getBgColor() - - return ( - fgColor === nullFg && - bgColor === nullBg && - !cell.isBold() && - !cell.isItalic() && - !cell.isUnderline() && - !cell.isBlink() && - !cell.isInverse() && - !cell.isInvisible() && - !cell.isDim() && - !cell.isStrikethrough() - ) - } - - protected _nextCell(cell: IBufferCell, _oldCell: IBufferCell, row: number, col: number): void { - const isPlaceHolderCell = cell.getWidth() === 0 - - if (isPlaceHolderCell) { - return - } - - const codepoint = cell.getCode() - const isInvalidCodepoint = codepoint > 0x10ffff || (codepoint >= 0xd800 && codepoint <= 0xdfff) - const isGarbage = isInvalidCodepoint || (codepoint >= 0xf000 && cell.getWidth() === 1) - const isEmptyCell = codepoint === 0 || cell.getChars() === "" || isGarbage - - const sgrSeq = this._diffStyle(cell, this._cursorStyle) - - const styleChanged = sgrSeq.length > 0 - - if (styleChanged) { - if (this._nullCellCount > 0) { - this._currentRow += " ".repeat(this._nullCellCount) - this._nullCellCount = 0 - } - - this._lastContentCursorRow = this._lastCursorRow = row - this._lastContentCursorCol = this._lastCursorCol = col - - this._currentRow += `\u001b[${sgrSeq.join(";")}m` - - const line = this._buffer.getLine(row) - const cellFromLine = line?.getCell(col) - if (cellFromLine) { - this._cursorStyle = cellFromLine - } - } - - if (isEmptyCell) { - this._nullCellCount += cell.getWidth() - } else { - if (this._nullCellCount > 0) { - this._currentRow += " ".repeat(this._nullCellCount) - this._nullCellCount = 0 - } - - this._currentRow += cell.getChars() - - this._lastContentCursorRow = this._lastCursorRow = row - this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth() - } - } - - protected _serializeString(excludeFinalCursorPosition?: boolean): string { - let rowEnd = this._allRows.length - - if (this._buffer.length - this._firstRow <= this._terminal.rows) { - rowEnd = this._lastContentCursorRow + 1 - this._firstRow - this._lastCursorCol = this._lastContentCursorCol - this._lastCursorRow = this._lastContentCursorRow - } - - let content = "" - - for (let i = 0; i < rowEnd; i++) { - content += this._allRows[i] - if (i + 1 < rowEnd) { - content += this._allRowSeparators[i] - } - } - - if (excludeFinalCursorPosition) return content - - const absoluteCursorRow = (this._buffer.baseY ?? 0) + this._buffer.cursorY - const cursorRow = constrain(absoluteCursorRow - this._firstRow + 1, 1, Number.MAX_SAFE_INTEGER) - const cursorCol = this._buffer.cursorX + 1 - content += `\u001b[${cursorRow};${cursorCol}H` - - const line = this._buffer.getLine(absoluteCursorRow) - const cell = line?.getCell(this._buffer.cursorX) - const style = (() => { - if (!cell) return this._buffer.getNullCell() - if (cell.getWidth() !== 0) return cell - if (this._buffer.cursorX > 0) return line?.getCell(this._buffer.cursorX - 1) ?? cell - return cell - })() - - const sgrSeq = this._diffStyle(style, this._cursorStyle) - if (sgrSeq.length) content += `\u001b[${sgrSeq.join(";")}m` - - return content - } -} - -// ============================================================================ -// SerializeAddon Class -// ============================================================================ - -export class SerializeAddon implements ITerminalAddon { - private _terminal?: ITerminalCore - - /** - * Activate the addon (called by Terminal.loadAddon) - */ - public activate(terminal: ITerminalCore): void { - this._terminal = terminal - } - - /** - * Dispose the addon and clean up resources - */ - public dispose(): void { - this._terminal = undefined - } - - /** - * Serializes terminal rows into a string that can be written back to the - * terminal to restore the state. The cursor will also be positioned to the - * correct cell. - * - * @param options Custom options to allow control over what gets serialized. - */ - public serialize(options?: ISerializeOptions): string { - if (!this._terminal) { - throw new Error("Cannot use addon until it has been loaded") - } - - const buffer = getTerminalBuffers(this._terminal) - - if (!buffer) { - return "" - } - - const normalBuffer = buffer.normal ?? buffer.active - const altBuffer = buffer.alternate - - if (!normalBuffer) { - return "" - } - - let content = options?.range - ? this._serializeBufferByRange(normalBuffer, options.range, true) - : this._serializeBufferByScrollback(normalBuffer, options?.scrollback) - - if (!options?.excludeAltBuffer && buffer.active?.type === "alternate" && altBuffer) { - const alternateContent = this._serializeBufferByScrollback(altBuffer, undefined) - content += `\u001b[?1049h\u001b[H${alternateContent}` - } - - return content - } - - /** - * Serializes terminal content as plain text (no escape sequences) - * @param options Custom options to allow control over what gets serialized. - */ - public serializeAsText(options?: { scrollback?: number; trimWhitespace?: boolean }): string { - if (!this._terminal) { - throw new Error("Cannot use addon until it has been loaded") - } - - const buffer = getTerminalBuffers(this._terminal) - - if (!buffer) { - return "" - } - - const activeBuffer = buffer.active ?? buffer.normal - if (!activeBuffer) { - return "" - } - - const maxRows = activeBuffer.length - const scrollback = options?.scrollback - const correctRows = scrollback === undefined ? maxRows : constrain(scrollback + this._terminal.rows, 0, maxRows) - - const startRow = maxRows - correctRows - const endRow = maxRows - 1 - const lines: string[] = [] - - for (let row = startRow; row <= endRow; row++) { - const line = activeBuffer.getLine(row) - if (line) { - const text = line.translateToString(options?.trimWhitespace ?? true) - lines.push(text) - } - } - - // Trim trailing empty lines if requested - if (options?.trimWhitespace) { - while (lines.length > 0 && lines[lines.length - 1] === "") { - lines.pop() - } - } - - return lines.join("\n") - } - - private _serializeBufferByScrollback(buffer: IBuffer, scrollback?: number): string { - const maxRows = buffer.length - const rows = this._terminal?.rows ?? 24 - const correctRows = scrollback === undefined ? maxRows : constrain(scrollback + rows, 0, maxRows) - return this._serializeBufferByRange( - buffer, - { - start: maxRows - correctRows, - end: maxRows - 1, - }, - false, - ) - } - - private _serializeBufferByRange( - buffer: IBuffer, - range: ISerializeRange, - excludeFinalCursorPosition: boolean, - ): string { - const handler = new StringSerializeHandler(buffer, this._terminal!) - const cols = this._terminal?.cols ?? 80 - return handler.serialize( - { - start: { x: 0, y: range.start }, - end: { x: cols, y: range.end }, - }, - excludeFinalCursorPosition, - ) - } -} diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx deleted file mode 100644 index 6bec1de84f0..00000000000 --- a/packages/app/src/app.tsx +++ /dev/null @@ -1,314 +0,0 @@ -import "@/index.css" -import { I18nProvider } from "@opencode-ai/ui/context" -import { DialogProvider } from "@opencode-ai/ui/context/dialog" -import { FileComponentProvider } from "@opencode-ai/ui/context/file" -import { MarkedProvider } from "@opencode-ai/ui/context/marked" -import { File } from "@opencode-ai/ui/file" -import { Font } from "@opencode-ai/ui/font" -import { Splash } from "@opencode-ai/ui/logo" -import { ThemeProvider } from "@opencode-ai/ui/theme/context" -import { MetaProvider } from "@solidjs/meta" -import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router" -import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" -import { Effect } from "effect" -import { - type Component, - createMemo, - createResource, - createSignal, - ErrorBoundary, - For, - type JSX, - lazy, - onCleanup, - type ParentProps, - Show, - Suspense, -} from "solid-js" -import { Dynamic } from "solid-js/web" -import { CommandProvider } from "@/context/command" -import { CommentsProvider } from "@/context/comments" -import { FileProvider } from "@/context/file" -import { GlobalSDKProvider } from "@/context/global-sdk" -import { GlobalSyncProvider } from "@/context/global-sync" -import { HighlightsProvider } from "@/context/highlights" -import { LanguageProvider, type Locale, useLanguage } from "@/context/language" -import { LayoutProvider } from "@/context/layout" -import { ModelsProvider } from "@/context/models" -import { NotificationProvider } from "@/context/notification" -import { PermissionProvider } from "@/context/permission" -import { PromptProvider } from "@/context/prompt" -import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server" -import { SettingsProvider } from "@/context/settings" -import { TerminalProvider } from "@/context/terminal" -import DirectoryLayout from "@/pages/directory-layout" -import Layout from "@/pages/layout" -import { ErrorPage } from "./pages/error" -import { useCheckServerHealth } from "./utils/server-health" - -const HomeRoute = lazy(() => import("@/pages/home")) -const loadSession = () => import("@/pages/session") -const Session = lazy(loadSession) -const Loading = () =>
- -if (typeof location === "object" && /\/session(?:\/|$)/.test(location.pathname)) { - void loadSession() -} - -const SessionRoute = () => ( - - - -) - -const SessionIndexRoute = () => - -function UiI18nBridge(props: ParentProps) { - const language = useLanguage() - return {props.children} -} - -declare global { - interface Window { - __KILO__?: { - updaterEnabled?: boolean - deepLinks?: string[] - wsl?: boolean - } - api?: { - setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise - } - } -} - -function QueryProvider(props: ParentProps) { - const client = new QueryClient() - return {props.children} -} - -function AppShellProviders(props: ParentProps) { - return ( - - - - - - - - {props.children} - - - - - - - - ) -} - -function SessionProviders(props: ParentProps) { - return ( - - - - {props.children} - - - - ) -} - -function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) { - return ( - - {/*}>*/} - {props.appChildren} - {props.children} - {/**/} - - ) -} - -export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) { - return ( - - - { - void window.api?.setTitlebar?.({ mode }) - }} - > - - - }> - - - {props.children} - - - - - - - - ) -} - -function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) { - const server = useServer() - const checkServerHealth = useCheckServerHealth() - - const [checkMode, setCheckMode] = createSignal<"blocking" | "background">("blocking") - - // performs repeated health check with a grace period for - // non-http connections, otherwise fails instantly - const [startupHealthCheck, healthCheckActions] = createResource(() => - props.disableHealthCheck - ? true - : Effect.gen(function* () { - if (!server.current) return true - const { http, type } = server.current - - while (true) { - const res = yield* Effect.promise(() => checkServerHealth(http)) - if (res.healthy) return true - if (checkMode() === "background" || type === "http") return false - } - }).pipe( - Effect.timeoutOrElse({ duration: "10 seconds", orElse: () => Effect.succeed(false) }), - Effect.ensuring(Effect.sync(() => setCheckMode("background"))), - Effect.runPromise, - ), - ) - - return ( - - -
- } - > - {/* - - - } - >*/} - {checkMode() === "blocking" ? startupHealthCheck() : startupHealthCheck.latest} - { - if (checkMode() === "background") void healthCheckActions.refetch() - }} - onServerSelected={(key) => { - setCheckMode("blocking") - server.setActive(key) - void healthCheckActions.refetch() - }} - /> - } - > - {props.children} - - {/**/} - - ) -} - -function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key: ServerConnection.Key) => void }) { - const language = useLanguage() - const server = useServer() - const others = () => server.list.filter((s) => ServerConnection.key(s) !== server.key) - const name = createMemo(() => server.name || server.key) - const serverToken = "\u0000server\u0000" - const unreachable = createMemo(() => language.t("app.server.unreachable", { server: serverToken }).split(serverToken)) - - const timer = setInterval(() => props.onRetry?.(), 1000) - onCleanup(() => clearInterval(timer)) - - return ( -
-
- -

- {unreachable()[0]} - {name()} - {unreachable()[1]} -

-

{language.t("app.server.retrying")}

-
- 0}> -
- {language.t("app.server.otherServers")} -
- - {(conn) => { - const key = ServerConnection.key(conn) - return ( - - ) - }} - -
-
-
-
- ) -} - -function ServerKey(props: ParentProps) { - const server = useServer() - return ( - - {props.children} - - ) -} - -export function AppInterface(props: { - children?: JSX.Element - defaultServer: ServerConnection.Key - servers?: Array - router?: Component - disableHealthCheck?: boolean -}) { - return ( - - - - - - - {routerProps.children}} - > - - - - - - - - - - - - - ) -} diff --git a/packages/app/src/components/debug-bar.tsx b/packages/app/src/components/debug-bar.tsx deleted file mode 100644 index 11f9f59e4e0..00000000000 --- a/packages/app/src/components/debug-bar.tsx +++ /dev/null @@ -1,443 +0,0 @@ -import { useIsRouting, useLocation } from "@solidjs/router" -import { batch, createEffect, onCleanup, onMount } from "solid-js" -import { createStore } from "solid-js/store" -import { makeEventListener } from "@solid-primitives/event-listener" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { useLanguage } from "@/context/language" - -type Mem = Performance & { - memory?: { - usedJSHeapSize: number - jsHeapSizeLimit: number - } -} - -type Evt = PerformanceEntry & { - interactionId?: number - processingStart?: number -} - -type Shift = PerformanceEntry & { - hadRecentInput: boolean - value: number -} - -type Obs = PerformanceObserverInit & { - durationThreshold?: number -} - -const span = 5000 - -const ms = (n?: number, d = 0) => { - if (n === undefined || Number.isNaN(n)) return - return `${n.toFixed(d)}ms` -} - -const time = (n?: number) => { - if (n === undefined || Number.isNaN(n)) return - return `${Math.round(n)}` -} - -const mb = (n?: number) => { - if (n === undefined || Number.isNaN(n)) return - const v = n / 1024 / 1024 - return `${v >= 1024 ? v.toFixed(0) : v.toFixed(1)}MB` -} - -const bad = (n: number | undefined, limit: number, low = false) => { - if (n === undefined || Number.isNaN(n)) return false - return low ? n < limit : n > limit -} - -const session = (path: string) => path.includes("/session") - -function Cell(props: { bad?: boolean; dim?: boolean; label: string; tip: string; value: string; wide?: boolean }) { - return ( - -
-
{props.label}
-
- {props.value} -
-
-
- ) -} - -export function DebugBar() { - const language = useLanguage() - const location = useLocation() - const routing = useIsRouting() - const [state, setState] = createStore({ - cls: undefined as number | undefined, - delay: undefined as number | undefined, - fps: undefined as number | undefined, - gap: undefined as number | undefined, - heap: { - limit: undefined as number | undefined, - used: undefined as number | undefined, - }, - inp: undefined as number | undefined, - jank: undefined as number | undefined, - long: { - block: undefined as number | undefined, - count: undefined as number | undefined, - max: undefined as number | undefined, - }, - nav: { - dur: undefined as number | undefined, - pending: false, - }, - }) - - const na = () => language.t("debugBar.na") - const heap = () => (state.heap.limit ? (state.heap.used ?? 0) / state.heap.limit : undefined) - const heapv = () => { - const value = heap() - if (value === undefined) return na() - return `${Math.round(value * 100)}%` - } - const longv = () => (state.long.count === undefined ? na() : `${time(state.long.block) ?? na()}/${state.long.count}`) - const navv = () => (state.nav.pending ? "..." : (time(state.nav.dur) ?? na())) - - let prev = "" - let start = 0 - let init = false - let one = 0 - let two = 0 - - createEffect(() => { - const busy = routing() - const next = `${location.pathname}${location.search}` - - if (!init) { - init = true - prev = next - return - } - - if (busy) { - if (one !== 0) cancelAnimationFrame(one) - if (two !== 0) cancelAnimationFrame(two) - one = 0 - two = 0 - if (start !== 0) return - start = performance.now() - if (session(prev)) setState("nav", { dur: undefined, pending: true }) - return - } - - if (start === 0) { - prev = next - return - } - - const at = start - const from = prev - start = 0 - prev = next - - if (!(session(from) || session(next))) return - - if (one !== 0) cancelAnimationFrame(one) - if (two !== 0) cancelAnimationFrame(two) - one = requestAnimationFrame(() => { - one = 0 - two = requestAnimationFrame(() => { - two = 0 - setState("nav", { dur: performance.now() - at, pending: false }) - }) - }) - }) - - onMount(() => { - const obs: PerformanceObserver[] = [] - const fps: Array<{ at: number; dur: number }> = [] - const long: Array<{ at: number; dur: number }> = [] - const seen = new Map() - let hasLong = false - let poll: number | undefined - let raf = 0 - let last = 0 - let snap = 0 - - const trim = (list: Array<{ at: number; dur: number }>, span: number, at: number) => { - while (list[0] && at - list[0].at > span) list.shift() - } - - const syncFrame = (at: number) => { - trim(fps, span, at) - const total = fps.reduce((sum, entry) => sum + entry.dur, 0) - const gap = fps.reduce((max, entry) => Math.max(max, entry.dur), 0) - const jank = fps.filter((entry) => entry.dur > 32).length - batch(() => { - setState("fps", total > 0 ? (fps.length * 1000) / total : undefined) - setState("gap", gap > 0 ? gap : undefined) - setState("jank", jank) - }) - } - - const syncLong = (at = performance.now()) => { - if (!hasLong) return - trim(long, span, at) - const block = long.reduce((sum, entry) => sum + Math.max(0, entry.dur - 50), 0) - const max = long.reduce((hi, entry) => Math.max(hi, entry.dur), 0) - setState("long", { block, count: long.length, max }) - } - - const syncInp = (at = performance.now()) => { - for (const [key, entry] of seen) { - if (at - entry.at > span) seen.delete(key) - } - let delay = 0 - let inp = 0 - for (const entry of seen.values()) { - delay = Math.max(delay, entry.delay) - inp = Math.max(inp, entry.dur) - } - batch(() => { - setState("delay", delay > 0 ? delay : undefined) - setState("inp", inp > 0 ? inp : undefined) - }) - } - - const syncHeap = () => { - const mem = (performance as Mem).memory - if (!mem) return - setState("heap", { limit: mem.jsHeapSizeLimit, used: mem.usedJSHeapSize }) - } - - const reset = () => { - fps.length = 0 - long.length = 0 - seen.clear() - last = 0 - snap = 0 - batch(() => { - setState("fps", undefined) - setState("gap", undefined) - setState("jank", undefined) - setState("delay", undefined) - setState("inp", undefined) - if (hasLong) setState("long", { block: 0, count: 0, max: 0 }) - }) - } - - const watch = (type: string, init: Obs, fn: (entries: PerformanceEntry[]) => void) => { - if (typeof PerformanceObserver === "undefined") return false - if (!(PerformanceObserver.supportedEntryTypes ?? []).includes(type)) return false - const ob = new PerformanceObserver((list) => fn(list.getEntries())) - try { - ob.observe(init) - obs.push(ob) - return true - } catch { - ob.disconnect() - return false - } - } - - if ( - watch("layout-shift", { buffered: true, type: "layout-shift" }, (entries) => { - const add = entries.reduce((sum, entry) => { - const item = entry as Shift - if (item.hadRecentInput) return sum - return sum + item.value - }, 0) - if (add === 0) return - setState("cls", (value) => (value ?? 0) + add) - }) - ) { - setState("cls", 0) - } - - if ( - watch("longtask", { buffered: true, type: "longtask" }, (entries) => { - const at = performance.now() - long.push(...entries.map((entry) => ({ at: entry.startTime, dur: entry.duration }))) - syncLong(at) - }) - ) { - hasLong = true - setState("long", { block: 0, count: 0, max: 0 }) - } - - watch("event", { buffered: true, durationThreshold: 16, type: "event" }, (entries) => { - for (const raw of entries) { - const entry = raw as Evt - if (entry.duration < 16) continue - const key = - entry.interactionId && entry.interactionId > 0 - ? entry.interactionId - : `${entry.name}:${Math.round(entry.startTime)}` - const prev = seen.get(key) - const delay = Math.max(0, (entry.processingStart ?? entry.startTime) - entry.startTime) - seen.set(key, { - at: entry.startTime, - delay: Math.max(prev?.delay ?? 0, delay), - dur: Math.max(prev?.dur ?? 0, entry.duration), - }) - if (seen.size <= 200) continue - const first = seen.keys().next().value - if (first !== undefined) seen.delete(first) - } - syncInp() - }) - - const loop = (at: number) => { - if (document.visibilityState !== "visible") { - raf = 0 - return - } - - if (last === 0) { - last = at - raf = requestAnimationFrame(loop) - return - } - - fps.push({ at, dur: at - last }) - last = at - - if (at - snap >= 250) { - snap = at - syncFrame(at) - } - - raf = requestAnimationFrame(loop) - } - - const stop = () => { - if (raf !== 0) cancelAnimationFrame(raf) - raf = 0 - if (poll === undefined) return - clearInterval(poll) - poll = undefined - } - - const start = () => { - if (document.visibilityState !== "visible") return - if (poll === undefined) { - poll = window.setInterval(() => { - syncLong() - syncInp() - syncHeap() - }, 1000) - } - if (raf !== 0) return - raf = requestAnimationFrame(loop) - } - - const vis = () => { - if (document.visibilityState !== "visible") { - stop() - return - } - reset() - start() - } - - syncHeap() - start() - makeEventListener(document, "visibilitychange", vis) - - onCleanup(() => { - if (one !== 0) cancelAnimationFrame(one) - if (two !== 0) cancelAnimationFrame(two) - stop() - for (const ob of obs) ob.disconnect() - }) - }) - - return ( - - ) -} diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx deleted file mode 100644 index 4822fa72666..00000000000 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ /dev/null @@ -1,656 +0,0 @@ -import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@kilocode/sdk/v2/client" -import { Button } from "@opencode-ai/ui/button" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { Dialog } from "@opencode-ai/ui/dialog" -import { Icon } from "@opencode-ai/ui/icon" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { List, type ListRef } from "@opencode-ai/ui/list" -import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { Spinner } from "@opencode-ai/ui/spinner" -import { TextField } from "@opencode-ai/ui/text-field" -import { showToast } from "@opencode-ai/ui/toast" -import { createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js" -import { createStore, produce } from "solid-js/store" -import { Link } from "@/components/link" -import { useGlobalSDK } from "@/context/global-sdk" -import { useGlobalSync } from "@/context/global-sync" -import { useLanguage } from "@/context/language" -import { useProviders } from "@/hooks/use-providers" - -export function DialogConnectProvider(props: { provider: string }) { - const dialog = useDialog() - const globalSync = useGlobalSync() - const globalSDK = useGlobalSDK() - const language = useLanguage() - const providers = useProviders() - - const all = () => { - void import("./dialog-select-provider").then((x) => { - dialog.show(() => ) - }) - } - - const alive = { value: true } - const timer = { current: undefined as ReturnType | undefined } - - onCleanup(() => { - alive.value = false - if (timer.current === undefined) return - clearTimeout(timer.current) - timer.current = undefined - }) - - const provider = createMemo( - () => - providers.all().find((x) => x.id === props.provider) ?? - globalSync.data.provider.all.find((x) => x.id === props.provider)!, - ) - const fallback = createMemo(() => [ - { - type: "api" as const, - label: language.t("provider.connect.method.apiKey"), - }, - ]) - const [auth] = createResource( - () => props.provider, - async () => { - const cached = globalSync.data.provider_auth[props.provider] - if (cached) return cached - const res = await globalSDK.client.provider.auth() - if (!alive.value) return fallback() - globalSync.set("provider_auth", res.data ?? {}) - return res.data?.[props.provider] ?? fallback() - }, - ) - const loading = createMemo(() => auth.loading && !globalSync.data.provider_auth[props.provider]) - const methods = createMemo(() => auth.latest ?? globalSync.data.provider_auth[props.provider] ?? fallback()) - const [store, setStore] = createStore({ - methodIndex: undefined as undefined | number, - authorization: undefined as undefined | ProviderAuthAuthorization, - state: "pending" as undefined | "pending" | "complete" | "error" | "prompt", - error: undefined as string | undefined, - }) - - type Action = - | { type: "method.select"; index: number } - | { type: "method.reset" } - | { type: "auth.prompt" } - | { type: "auth.pending" } - | { type: "auth.complete"; authorization: ProviderAuthAuthorization } - | { type: "auth.error"; error: string } - - function dispatch(action: Action) { - setStore( - produce((draft) => { - if (action.type === "method.select") { - draft.methodIndex = action.index - draft.authorization = undefined - draft.state = undefined - draft.error = undefined - return - } - if (action.type === "method.reset") { - draft.methodIndex = undefined - draft.authorization = undefined - draft.state = undefined - draft.error = undefined - return - } - if (action.type === "auth.prompt") { - draft.state = "prompt" - draft.error = undefined - return - } - if (action.type === "auth.pending") { - draft.state = "pending" - draft.error = undefined - return - } - if (action.type === "auth.complete") { - draft.state = "complete" - draft.authorization = action.authorization - draft.error = undefined - return - } - draft.state = "error" - draft.error = action.error - }), - ) - } - - const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined)) - - const methodLabel = (value?: { type?: string; label?: string }) => { - if (!value) return "" - if (value.type === "api") return language.t("provider.connect.method.apiKey") - return value.label ?? "" - } - - function formatError(value: unknown, fallback: string): string { - if (value && typeof value === "object" && "data" in value) { - const data = (value as { data?: { message?: unknown } }).data - if (typeof data?.message === "string" && data.message) return data.message - } - if (value && typeof value === "object" && "error" in value) { - const nested = formatError((value as { error?: unknown }).error, "") - if (nested) return nested - } - if (value && typeof value === "object" && "message" in value) { - const message = (value as { message?: unknown }).message - if (typeof message === "string" && message) return message - } - if (value instanceof Error && value.message) return value.message - if (typeof value === "string" && value) return value - return fallback - } - - async function selectMethod(index: number, inputs?: Record) { - if (timer.current !== undefined) { - clearTimeout(timer.current) - timer.current = undefined - } - - const method = methods()[index] - dispatch({ type: "method.select", index }) - - if (method.type === "oauth") { - if (method.prompts?.length && !inputs) { - dispatch({ type: "auth.prompt" }) - return - } - dispatch({ type: "auth.pending" }) - const start = Date.now() - await globalSDK.client.provider.oauth - .authorize( - { - providerID: props.provider, - method: index, - inputs, - }, - { throwOnError: true }, - ) - .then((x) => { - if (!alive.value) return - const elapsed = Date.now() - start - const delay = 1000 - elapsed - - if (delay > 0) { - if (timer.current !== undefined) clearTimeout(timer.current) - timer.current = setTimeout(() => { - timer.current = undefined - if (!alive.value) return - dispatch({ type: "auth.complete", authorization: x.data! }) - }, delay) - return - } - dispatch({ type: "auth.complete", authorization: x.data! }) - }) - .catch((e) => { - if (!alive.value) return - dispatch({ type: "auth.error", error: formatError(e, language.t("common.requestFailed")) }) - }) - } - } - - function OAuthPromptsView() { - const [formStore, setFormStore] = createStore({ - value: {} as Record, - index: 0, - }) - - const prompts = createMemo>(() => { - const value = method() - if (value?.type !== "oauth") return [] - return value.prompts ?? [] - }) - const matches = (prompt: NonNullable[number]>, value: Record) => { - if (!prompt.when) return true - const actual = value[prompt.when.key] - if (actual === undefined) return false - return prompt.when.op === "eq" ? actual === prompt.when.value : actual !== prompt.when.value - } - const current = createMemo(() => { - const all = prompts() - const index = all.findIndex((prompt, index) => index >= formStore.index && matches(prompt, formStore.value)) - if (index === -1) return - return { - index, - prompt: all[index], - } - }) - const valid = createMemo(() => { - const item = current() - if (!item || item.prompt.type !== "text") return false - const value = formStore.value[item.prompt.key] ?? "" - return value.trim().length > 0 - }) - - async function next(index: number, value: Record) { - if (store.methodIndex === undefined) return - const next = prompts().findIndex((prompt, i) => i > index && matches(prompt, value)) - if (next !== -1) { - setFormStore("index", next) - return - } - await selectMethod(store.methodIndex, value) - } - - async function handleSubmit(e: SubmitEvent) { - e.preventDefault() - const item = current() - if (!item || item.prompt.type !== "text") return - if (!valid()) return - await next(item.index, formStore.value) - } - - const item = () => current() - const text = createMemo(() => { - const prompt = item()?.prompt - if (!prompt || prompt.type !== "text") return - return prompt - }) - const select = createMemo(() => { - const prompt = item()?.prompt - if (!prompt || prompt.type !== "select") return - return prompt - }) - - return ( -
- - - { - const prompt = text() - if (!prompt) return - setFormStore("value", prompt.key, value) - }} - /> - - - -
-
{select()?.message}
-
- x.value} - current={select()?.options.find((x) => x.value === formStore.value[select()!.key])} - onSelect={(value) => { - if (!value) return - const prompt = select() - if (!prompt) return - const nextValue = { - ...formStore.value, - [prompt.key]: value.value, - } - setFormStore("value", prompt.key, value.value) - void next(item()!.index, nextValue) - }} - > - {(option) => ( -
-
- - {option.label} - {option.hint} -
- )} - -
-
- - - - ) - } - - let listRef: ListRef | undefined - function handleKey(e: KeyboardEvent) { - if (e.key === "Enter" && e.target instanceof HTMLInputElement) { - return - } - if (e.key === "Escape") return - listRef?.onKeyDown(e) - } - - let auto = false - createEffect(() => { - if (auto) return - if (loading()) return - if (methods().length === 1) { - auto = true - void selectMethod(0) - } - }) - - async function complete() { - await globalSDK.client.global.dispose() - dialog.close() - showToast({ - variant: "success", - icon: "circle-check", - title: language.t("provider.connect.toast.connected.title", { provider: provider().name }), - description: language.t("provider.connect.toast.connected.description", { provider: provider().name }), - }) - } - - function goBack() { - if (methods().length === 1) { - all() - return - } - if (store.authorization) { - dispatch({ type: "method.reset" }) - return - } - if (store.methodIndex !== undefined) { - dispatch({ type: "method.reset" }) - return - } - all() - } - - function MethodSelection() { - return ( - <> -
- {language.t("provider.connect.selectMethod", { provider: provider().name })} -
-
- { - listRef = ref - }} - items={methods} - key={(m) => m?.label} - onSelect={async (selected, index) => { - if (!selected) return - void selectMethod(index) - }} - > - {(i) => ( -
-
- - {methodLabel(i)} -
- )} - -
- - ) - } - - function ApiAuthView() { - const [formStore, setFormStore] = createStore({ - value: "", - error: undefined as string | undefined, - }) - - async function handleSubmit(e: SubmitEvent) { - e.preventDefault() - - const form = e.currentTarget as HTMLFormElement - const formData = new FormData(form) - const apiKey = formData.get("apiKey") as string - - if (!apiKey?.trim()) { - setFormStore("error", language.t("provider.connect.apiKey.required")) - return - } - - setFormStore("error", undefined) - await globalSDK.client.auth.set({ - providerID: props.provider, - auth: { - type: "api", - key: apiKey, - }, - }) - await complete() - } - - return ( -
- - {/* kilocode_change start */} - -
-
{language.t("provider.connect.kiloGateway.line1")}
-
{language.t("provider.connect.kiloGateway.line2")}
-
- {language.t("provider.connect.kiloGateway.visit.prefix")} - - {language.t("provider.connect.kiloGateway.visit.link")} - - {language.t("provider.connect.kiloGateway.visit.suffix")} -
-
-
- {/* kilocode_change end */} - -
- {language.t("provider.connect.apiKey.description", { provider: provider().name })} -
-
-
-
- setFormStore("value", v)} - validationState={formStore.error ? "invalid" : undefined} - error={formStore.error} - /> - - -
- ) - } - - function OAuthCodeView() { - const [formStore, setFormStore] = createStore({ - value: "", - error: undefined as string | undefined, - }) - - async function handleSubmit(e: SubmitEvent) { - e.preventDefault() - - const form = e.currentTarget as HTMLFormElement - const formData = new FormData(form) - const code = formData.get("code") as string - - if (!code?.trim()) { - setFormStore("error", language.t("provider.connect.oauth.code.required")) - return - } - - setFormStore("error", undefined) - const result = await globalSDK.client.provider.oauth - .callback({ - providerID: props.provider, - method: store.methodIndex, - code, - }) - .then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const })) - .catch((error) => ({ ok: false as const, error })) - if (result.ok) { - await complete() - return - } - setFormStore("error", formatError(result.error, language.t("provider.connect.oauth.code.invalid"))) - } - - return ( -
-
- {language.t("provider.connect.oauth.code.visit.prefix")} - {language.t("provider.connect.oauth.code.visit.link")} - {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })} -
-
- setFormStore("value", v)} - validationState={formStore.error ? "invalid" : undefined} - error={formStore.error} - /> - - -
- ) - } - - function OAuthAutoView() { - const code = createMemo(() => { - const instructions = store.authorization?.instructions - if (instructions?.includes(":")) { - return instructions.split(":")[1]?.trim() - } - return instructions - }) - - onMount(() => { - void (async () => { - const result = await globalSDK.client.provider.oauth - .callback({ - providerID: props.provider, - method: store.methodIndex, - }) - .then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const })) - .catch((error) => ({ ok: false as const, error })) - - if (!alive.value) return - - if (!result.ok) { - const message = formatError(result.error, language.t("common.requestFailed")) - dispatch({ type: "auth.error", error: message }) - return - } - - await complete() - })() - }) - - return ( -
-
- {language.t("provider.connect.oauth.auto.visit.prefix")} - {language.t("provider.connect.oauth.auto.visit.link")} - {language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })} -
- -
- - {language.t("provider.connect.status.waiting")} -
-
- ) - } - - return ( - - } - > -
-
- -
- - - {language.t("provider.connect.title.anthropicProMax")} - - {language.t("provider.connect.title", { provider: provider().name })} - -
-
-
-
- - -
-
- - {language.t("provider.connect.status.inProgress")} -
-
-
- - - - -
-
- - {language.t("provider.connect.status.inProgress")} -
-
-
- - - - -
-
- - {language.t("provider.connect.status.failed", { error: store.error ?? "" })} -
-
-
- - - - - - - - - - - - - -
-
-
-
-
- ) -} diff --git a/packages/app/src/components/dialog-custom-provider-form.ts b/packages/app/src/components/dialog-custom-provider-form.ts deleted file mode 100644 index e26dcb09710..00000000000 --- a/packages/app/src/components/dialog-custom-provider-form.ts +++ /dev/null @@ -1,158 +0,0 @@ -const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/ -const OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible" - -type Translator = (key: string, vars?: Record) => string - -export type ModelErr = { - id?: string - name?: string -} - -export type HeaderErr = { - key?: string - value?: string -} - -export type ModelRow = { - row: string - id: string - name: string - err: ModelErr -} - -export type HeaderRow = { - row: string - key: string - value: string - err: HeaderErr -} - -export type FormState = { - providerID: string - name: string - baseURL: string - apiKey: string - models: ModelRow[] - headers: HeaderRow[] - err: { - providerID?: string - name?: string - baseURL?: string - } -} - -type ValidateArgs = { - form: FormState - t: Translator - disabledProviders: string[] - existingProviderIDs: Set -} - -export function validateCustomProvider(input: ValidateArgs) { - const providerID = input.form.providerID.trim() - const name = input.form.name.trim() - const baseURL = input.form.baseURL.trim() - const apiKey = input.form.apiKey.trim() - - const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim() - const key = apiKey && !env ? apiKey : undefined - - const idError = !providerID - ? input.t("provider.custom.error.providerID.required") - : !PROVIDER_ID.test(providerID) - ? input.t("provider.custom.error.providerID.format") - : undefined - - const nameError = !name ? input.t("provider.custom.error.name.required") : undefined - const urlError = !baseURL - ? input.t("provider.custom.error.baseURL.required") - : !/^https?:\/\//.test(baseURL) - ? input.t("provider.custom.error.baseURL.format") - : undefined - - const disabled = input.disabledProviders.includes(providerID) - const existsError = idError - ? undefined - : input.existingProviderIDs.has(providerID) && !disabled - ? input.t("provider.custom.error.providerID.exists") - : undefined - - const seenModels = new Set() - const models = input.form.models.map((m) => { - const id = m.id.trim() - const idError = !id - ? input.t("provider.custom.error.required") - : seenModels.has(id) - ? input.t("provider.custom.error.duplicate") - : (() => { - seenModels.add(id) - return undefined - })() - const nameError = !m.name.trim() ? input.t("provider.custom.error.required") : undefined - return { id: idError, name: nameError } - }) - const modelsValid = models.every((m) => !m.id && !m.name) - const modelConfig = Object.fromEntries(input.form.models.map((m) => [m.id.trim(), { name: m.name.trim() }])) - - const seenHeaders = new Set() - const headers = input.form.headers.map((h) => { - const key = h.key.trim() - const value = h.value.trim() - - if (!key && !value) return {} - const keyError = !key - ? input.t("provider.custom.error.required") - : seenHeaders.has(key.toLowerCase()) - ? input.t("provider.custom.error.duplicate") - : (() => { - seenHeaders.add(key.toLowerCase()) - return undefined - })() - const valueError = !value ? input.t("provider.custom.error.required") : undefined - return { key: keyError, value: valueError } - }) - const headersValid = headers.every((h) => !h.key && !h.value) - const headerConfig = Object.fromEntries( - input.form.headers - .map((h) => ({ key: h.key.trim(), value: h.value.trim() })) - .filter((h) => !!h.key && !!h.value) - .map((h) => [h.key, h.value]), - ) - - const err = { - providerID: idError ?? existsError, - name: nameError, - baseURL: urlError, - } - - const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid - if (!ok) return { err, models, headers } - - return { - err, - models, - headers, - result: { - providerID, - name, - key, - config: { - npm: OPENAI_COMPATIBLE, - name, - ...(env ? { env: [env] } : {}), - options: { - baseURL, - ...(Object.keys(headerConfig).length ? { headers: headerConfig } : {}), - }, - models: modelConfig, - }, - }, - } -} - -let row = 0 - -const nextRow = () => `row-${row++}` - -export const modelRow = (): ModelRow => ({ row: nextRow(), id: "", name: "", err: {} }) -export const headerRow = (): HeaderRow => ({ row: nextRow(), key: "", value: "", err: {} }) diff --git a/packages/app/src/components/dialog-custom-provider.test.ts b/packages/app/src/components/dialog-custom-provider.test.ts deleted file mode 100644 index 07dd26ecd67..00000000000 --- a/packages/app/src/components/dialog-custom-provider.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { validateCustomProvider } from "./dialog-custom-provider-form" - -const t = (key: string) => key - -describe("validateCustomProvider", () => { - test("builds trimmed config payload", () => { - const result = validateCustomProvider({ - form: { - providerID: "custom-provider", - name: " Custom Provider ", - baseURL: "https://api.example.com ", - apiKey: " {env: CUSTOM_PROVIDER_KEY} ", - models: [{ row: "m0", id: " model-a ", name: " Model A ", err: {} }], - headers: [ - { row: "h0", key: " X-Test ", value: " enabled ", err: {} }, - { row: "h1", key: "", value: "", err: {} }, - ], - err: {}, - }, - t, - disabledProviders: [], - existingProviderIDs: new Set(), - }) - - expect(result.result).toEqual({ - providerID: "custom-provider", - name: "Custom Provider", - key: undefined, - config: { - npm: "@ai-sdk/openai-compatible", - name: "Custom Provider", - env: ["CUSTOM_PROVIDER_KEY"], - options: { - baseURL: "https://api.example.com", - headers: { - "X-Test": "enabled", - }, - }, - models: { - "model-a": { name: "Model A" }, - }, - }, - }) - }) - - test("flags duplicate rows and allows reconnecting disabled providers", () => { - const result = validateCustomProvider({ - form: { - providerID: "custom-provider", - name: "Provider", - baseURL: "https://api.example.com", - apiKey: "secret", - models: [ - { row: "m0", id: "model-a", name: "Model A", err: {} }, - { row: "m1", id: "model-a", name: "Model A 2", err: {} }, - ], - headers: [ - { row: "h0", key: "Authorization", value: "one", err: {} }, - { row: "h1", key: "authorization", value: "two", err: {} }, - ], - err: {}, - }, - t, - disabledProviders: ["custom-provider"], - existingProviderIDs: new Set(["custom-provider"]), - }) - - expect(result.result).toBeUndefined() - expect(result.err.providerID).toBeUndefined() - expect(result.models[1]).toEqual({ - id: "provider.custom.error.duplicate", - name: undefined, - }) - expect(result.headers[1]).toEqual({ - key: "provider.custom.error.duplicate", - value: undefined, - }) - }) -}) diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx deleted file mode 100644 index 6f38651e6d5..00000000000 --- a/packages/app/src/components/dialog-custom-provider.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import { Button } from "@opencode-ai/ui/button" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { Dialog } from "@opencode-ai/ui/dialog" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { useMutation } from "@tanstack/solid-query" -import { TextField } from "@opencode-ai/ui/text-field" -import { showToast } from "@opencode-ai/ui/toast" -import { batch, For } from "solid-js" -import { createStore, produce } from "solid-js/store" -import { Link } from "@/components/link" -import { useGlobalSDK } from "@/context/global-sdk" -import { useGlobalSync } from "@/context/global-sync" -import { useLanguage } from "@/context/language" -import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form" -import { DialogSelectProvider } from "./dialog-select-provider" - -type Props = { - back?: "providers" | "close" -} - -export function DialogCustomProvider(props: Props) { - const dialog = useDialog() - const globalSync = useGlobalSync() - const globalSDK = useGlobalSDK() - const language = useLanguage() - - const [form, setForm] = createStore({ - providerID: "", - name: "", - baseURL: "", - apiKey: "", - models: [modelRow()], - headers: [headerRow()], - err: {}, - }) - - const goBack = () => { - if (props.back === "close") { - dialog.close() - return - } - dialog.show(() => ) - } - - const addModel = () => { - setForm( - "models", - produce((rows) => { - rows.push(modelRow()) - }), - ) - } - - const removeModel = (index: number) => { - if (form.models.length <= 1) return - setForm( - "models", - produce((rows) => { - rows.splice(index, 1) - }), - ) - } - - const addHeader = () => { - setForm( - "headers", - produce((rows) => { - rows.push(headerRow()) - }), - ) - } - - const removeHeader = (index: number) => { - if (form.headers.length <= 1) return - setForm( - "headers", - produce((rows) => { - rows.splice(index, 1) - }), - ) - } - - const setField = (key: "providerID" | "name" | "baseURL" | "apiKey", value: string) => { - setForm(key, value) - if (key === "apiKey") return - setForm("err", key, undefined) - } - - const setModel = (index: number, key: "id" | "name", value: string) => { - batch(() => { - setForm("models", index, key, value) - setForm("models", index, "err", key, undefined) - }) - } - - const setHeader = (index: number, key: "key" | "value", value: string) => { - batch(() => { - setForm("headers", index, key, value) - setForm("headers", index, "err", key, undefined) - }) - } - - const validate = () => { - const output = validateCustomProvider({ - form, - t: language.t, - disabledProviders: globalSync.data.config.disabled_providers ?? [], - existingProviderIDs: new Set(globalSync.data.provider.all.map((p) => p.id)), - }) - batch(() => { - setForm("err", output.err) - output.models.forEach((err, index) => setForm("models", index, "err", err)) - output.headers.forEach((err, index) => setForm("headers", index, "err", err)) - }) - return output.result - } - - const saveMutation = useMutation(() => ({ - mutationFn: async (result: NonNullable>) => { - const disabledProviders = globalSync.data.config.disabled_providers ?? [] - const nextDisabled = disabledProviders.filter((id) => id !== result.providerID) - - if (result.key) { - await globalSDK.client.auth.set({ - providerID: result.providerID, - auth: { - type: "api", - key: result.key, - }, - }) - } - - await globalSync.updateConfig({ - provider: { [result.providerID]: result.config }, - disabled_providers: nextDisabled, - }) - return result - }, - onSuccess: (result) => { - dialog.close() - showToast({ - variant: "success", - icon: "circle-check", - title: language.t("provider.connect.toast.connected.title", { provider: result.name }), - description: language.t("provider.connect.toast.connected.description", { provider: result.name }), - }) - }, - onError: (err) => { - const message = err instanceof Error ? err.message : String(err) - showToast({ title: language.t("common.requestFailed"), description: message }) - }, - })) - - const save = (e: SubmitEvent) => { - e.preventDefault() - if (saveMutation.isPending) return - - const result = validate() - if (!result) return - saveMutation.mutate(result) - } - - return ( - - } - transition - > -
-
- -
{language.t("provider.custom.title")}
-
- -
-

- {language.t("provider.custom.description.prefix")} - - {language.t("provider.custom.description.link")} - - {language.t("provider.custom.description.suffix")} -

- -
- setField("providerID", v)} - validationState={form.err.providerID ? "invalid" : undefined} - error={form.err.providerID} - /> - setField("name", v)} - validationState={form.err.name ? "invalid" : undefined} - error={form.err.name} - /> - setField("baseURL", v)} - validationState={form.err.baseURL ? "invalid" : undefined} - error={form.err.baseURL} - /> - setField("apiKey", v)} - /> -
- -
- - - {(m, i) => ( -
-
- setModel(i(), "id", v)} - validationState={m.err.id ? "invalid" : undefined} - error={m.err.id} - /> -
-
- setModel(i(), "name", v)} - validationState={m.err.name ? "invalid" : undefined} - error={m.err.name} - /> -
- removeModel(i())} - disabled={form.models.length <= 1} - aria-label={language.t("provider.custom.models.remove")} - /> -
- )} -
- -
- -
- - - {(h, i) => ( -
-
- setHeader(i(), "key", v)} - validationState={h.err.key ? "invalid" : undefined} - error={h.err.key} - /> -
-
- setHeader(i(), "value", v)} - validationState={h.err.value ? "invalid" : undefined} - error={h.err.value} - /> -
- removeHeader(i())} - disabled={form.headers.length <= 1} - aria-label={language.t("provider.custom.headers.remove")} - /> -
- )} -
- -
- - -
-
-
- ) -} diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx deleted file mode 100644 index 8eb12daf52e..00000000000 --- a/packages/app/src/components/dialog-edit-project.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import { Button } from "@opencode-ai/ui/button" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { Dialog } from "@opencode-ai/ui/dialog" -import { TextField } from "@opencode-ai/ui/text-field" -import { useMutation } from "@tanstack/solid-query" -import { Icon } from "@opencode-ai/ui/icon" -import { createMemo, For, Show } from "solid-js" -import { createStore } from "solid-js/store" -import { useGlobalSDK } from "@/context/global-sdk" -import { useGlobalSync } from "@/context/global-sync" -import { type LocalProject, getAvatarColors } from "@/context/layout" -import { getFilename } from "@opencode-ai/shared/util/path" -import { Avatar } from "@opencode-ai/ui/avatar" -import { useLanguage } from "@/context/language" -import { getProjectAvatarSource } from "@/pages/layout/sidebar-items" - -const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const - -export function DialogEditProject(props: { project: LocalProject }) { - const dialog = useDialog() - const globalSDK = useGlobalSDK() - const globalSync = useGlobalSync() - const language = useLanguage() - - const folderName = createMemo(() => getFilename(props.project.worktree)) - const defaultName = createMemo(() => props.project.name || folderName()) - - const [store, setStore] = createStore({ - name: defaultName(), - color: props.project.icon?.color, - iconOverride: props.project.icon?.override, - startup: props.project.commands?.start ?? "", - dragOver: false, - iconHover: false, - }) - - let iconInput: HTMLInputElement | undefined - - function handleFileSelect(file: File) { - if (!file.type.startsWith("image/")) return - const reader = new FileReader() - reader.onload = (e) => { - setStore("iconOverride", e.target?.result as string) - setStore("iconHover", false) - } - reader.readAsDataURL(file) - } - - function handleDrop(e: DragEvent) { - e.preventDefault() - setStore("dragOver", false) - const file = e.dataTransfer?.files[0] - if (file) handleFileSelect(file) - } - - function handleDragOver(e: DragEvent) { - e.preventDefault() - setStore("dragOver", true) - } - - function handleDragLeave() { - setStore("dragOver", false) - } - - function handleInputChange(e: Event) { - const input = e.target as HTMLInputElement - const file = input.files?.[0] - if (file) handleFileSelect(file) - } - - function clearIcon() { - setStore("iconOverride", "") - } - - const saveMutation = useMutation(() => ({ - mutationFn: async () => { - const name = store.name.trim() === folderName() ? "" : store.name.trim() - const start = store.startup.trim() - - if (props.project.id && props.project.id !== "global") { - await globalSDK.client.project.update({ - projectID: props.project.id, - directory: props.project.worktree, - name, - icon: { color: store.color || "", override: store.iconOverride || "" }, - commands: { start }, - }) - globalSync.project.icon(props.project.worktree, store.iconOverride || undefined) - dialog.close() - return - } - - globalSync.project.meta(props.project.worktree, { - name, - icon: { color: store.color || undefined, override: store.iconOverride || undefined }, - commands: { start: start || undefined }, - }) - dialog.close() - }, - })) - - function handleSubmit(e: SubmitEvent) { - e.preventDefault() - if (saveMutation.isPending) return - saveMutation.mutate() - } - - return ( - -
-
- setStore("name", v)} - /> - -
- -
-
setStore("iconHover", true)} - onMouseLeave={() => setStore("iconHover", false)} - > -
{ - if (store.iconOverride && store.iconHover) { - clearIcon() - } else { - iconInput?.click() - } - }} - > - - -
- } - > - {(src) => ( - {language.t("dialog.project.edit.icon.alt")} - )} - -
-
- -
-
- -
-
- { - iconInput = el - }} - type="file" - accept="image/*" - class="hidden" - onChange={handleInputChange} - /> -
- {language.t("dialog.project.edit.icon.hint")} - {language.t("dialog.project.edit.icon.recommended")} -
-
-
- - -
- -
- - {(color) => ( - - )} - -
-
-
- - setStore("startup", v)} - spellcheck={false} - class="max-h-14 w-full overflow-y-auto font-mono text-xs" - /> -
- -
- - -
- - - ) -} diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx deleted file mode 100644 index 4cdcca33ac1..00000000000 --- a/packages/app/src/components/dialog-fork.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { Component, createMemo } from "solid-js" -import { useNavigate, useParams } from "@solidjs/router" -import { useSync } from "@/context/sync" -import { useSDK } from "@/context/sdk" -import { usePrompt } from "@/context/prompt" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { Dialog } from "@opencode-ai/ui/dialog" -import { List } from "@opencode-ai/ui/list" -import { showToast } from "@opencode-ai/ui/toast" -import { extractPromptFromParts } from "@/utils/prompt" -import type { TextPart as SDKTextPart } from "@kilocode/sdk/v2/client" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { useLanguage } from "@/context/language" - -interface ForkableMessage { - id: string - text: string - time: string -} - -function formatTime(date: Date): string { - return date.toLocaleTimeString(undefined, { timeStyle: "short" }) -} - -export const DialogFork: Component = () => { - const params = useParams() - const navigate = useNavigate() - const sync = useSync() - const sdk = useSDK() - const prompt = usePrompt() - const dialog = useDialog() - const language = useLanguage() - - const messages = createMemo((): ForkableMessage[] => { - const sessionID = params.id - if (!sessionID) return [] - - const msgs = sync.data.message[sessionID] ?? [] - const result: ForkableMessage[] = [] - - for (const message of msgs) { - if (message.role !== "user") continue - - const parts = sync.data.part[message.id] ?? [] - const textPart = parts.find((x): x is SDKTextPart => x.type === "text" && !x.synthetic && !x.ignored) - if (!textPart) continue - - result.push({ - id: message.id, - text: textPart.text.replace(/\n/g, " ").slice(0, 200), - time: formatTime(new Date(message.time.created)), - }) - } - - return result.reverse() - }) - - const handleSelect = (item: ForkableMessage | undefined) => { - if (!item) return - - const sessionID = params.id - if (!sessionID) return - - const parts = sync.data.part[item.id] ?? [] - const restored = extractPromptFromParts(parts, { - directory: sdk.directory, - attachmentName: language.t("common.attachment"), - }) - const dir = base64Encode(sdk.directory) - - sdk.client.session - .fork({ sessionID, messageID: item.id }) - .then((forked) => { - if (!forked.data) { - showToast({ title: language.t("common.requestFailed") }) - return - } - dialog.close() - prompt.set(restored, undefined, { dir, id: forked.data.id }) - navigate(`/${dir}/session/${forked.data.id}`) - }) - .catch((err: unknown) => { - const message = err instanceof Error ? err.message : String(err) - showToast({ title: language.t("common.requestFailed"), description: message }) - }) - } - - return ( - - x.id} - items={messages} - filterKeys={["text"]} - onSelect={handleSelect} - > - {(item) => ( -
- {item.text} - {item.time} -
- )} -
-
- ) -} diff --git a/packages/app/src/components/dialog-manage-models.tsx b/packages/app/src/components/dialog-manage-models.tsx deleted file mode 100644 index ace79e38a7c..00000000000 --- a/packages/app/src/components/dialog-manage-models.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Dialog } from "@opencode-ai/ui/dialog" -import { List } from "@opencode-ai/ui/list" -import { Switch } from "@opencode-ai/ui/switch" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { Button } from "@opencode-ai/ui/button" -import type { Component } from "solid-js" -import { useLocal } from "@/context/local" -import { popularProviders } from "@/hooks/use-providers" -import { useLanguage } from "@/context/language" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { DialogSelectProvider } from "./dialog-select-provider" - -export const DialogManageModels: Component = () => { - const local = useLocal() - const language = useLanguage() - const dialog = useDialog() - - const handleConnectProvider = () => { - dialog.show(() => ) - } - const providerRank = (id: string) => popularProviders.indexOf(id) - const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID) - const providerVisible = (providerID: string) => - providerList(providerID).every((x) => local.model.visible({ modelID: x.id, providerID: x.provider.id })) - const setProviderVisibility = (providerID: string, checked: boolean) => { - providerList(providerID).forEach((x) => { - local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, checked) - }) - } - - return ( - - {language.t("command.provider.connect")} - - } - > - `${x?.provider?.id}:${x?.id}`} - items={local.model.list()} - filterKeys={["provider.name", "name", "id"]} - sortBy={(a, b) => a.name.localeCompare(b.name)} - groupBy={(x) => x.provider.id} - groupHeader={(group) => { - const provider = group.items[0].provider - return ( - <> - {provider.name} - - setProviderVisibility(provider.id, checked)} - hideLabel - > - {provider.name} - - - - ) - }} - sortGroupsBy={(a, b) => { - const aRank = providerRank(a.items[0].provider.id) - const bRank = providerRank(b.items[0].provider.id) - const aPopular = aRank >= 0 - const bPopular = bRank >= 0 - if (aPopular && !bPopular) return -1 - if (!aPopular && bPopular) return 1 - return aRank - bRank - }} - onSelect={(x) => { - if (!x) return - const key = { modelID: x.id, providerID: x.provider.id } - local.model.setVisibility(key, !local.model.visible(key)) - }} - > - {(i) => ( -
- {i.name} -
e.stopPropagation()}> - { - local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked) - }} - /> -
-
- )} -
-
- ) -} diff --git a/packages/app/src/components/dialog-release-notes.tsx b/packages/app/src/components/dialog-release-notes.tsx deleted file mode 100644 index d0a35b71beb..00000000000 --- a/packages/app/src/components/dialog-release-notes.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { createSignal } from "solid-js" -import { Dialog } from "@opencode-ai/ui/dialog" -import { Button } from "@opencode-ai/ui/button" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { useLanguage } from "@/context/language" -import { useSettings } from "@/context/settings" - -export type Highlight = { - title: string - description: string - media?: { - type: "image" | "video" - src: string - alt?: string - } -} - -export function DialogReleaseNotes(props: { highlights: Highlight[] }) { - const dialog = useDialog() - const language = useLanguage() - const settings = useSettings() - const [index, setIndex] = createSignal(0) - - const total = () => props.highlights.length - const last = () => Math.max(0, total() - 1) - const feature = () => props.highlights[index()] ?? props.highlights[last()] - const isFirst = () => index() === 0 - const isLast = () => index() >= last() - const paged = () => total() > 1 - - function handleNext() { - if (isLast()) return - setIndex(index() + 1) - } - - function handleClose() { - dialog.close() - } - - function handleDisable() { - settings.general.setReleaseNotes(false) - handleClose() - } - - function handleKeyDown(e: KeyboardEvent) { - if (e.key === "Escape") { - e.preventDefault() - handleClose() - return - } - - if (!paged()) return - if (e.key === "ArrowLeft" && !isFirst()) { - e.preventDefault() - setIndex(index() - 1) - } - if (e.key === "ArrowRight" && !isLast()) { - e.preventDefault() - setIndex(index() + 1) - } - } - - return ( - -
- {/* Left side - Text content */} -
- {/* Top section - feature content (fixed position from top) */} -
-
-

{feature()?.title ?? ""}

-
-

{feature()?.description ?? ""}

-
- - {/* Spacer to push buttons to bottom */} -
- - {/* Bottom section - buttons and indicators (fixed position) */} -
-
- {isLast() ? ( - - ) : ( - - )} - - -
- - {paged() && ( -
- {props.highlights.map((_, i) => ( - - ))} -
- )} -
-
- - {/* Right side - Media content (edge to edge) */} - {feature()?.media && ( -
- {feature()!.media!.type === "image" ? ( - {feature()!.media!.alt - ) : ( -
- )} -
-
- ) -} diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx deleted file mode 100644 index 903cb1915da..00000000000 --- a/packages/app/src/components/dialog-select-directory.tsx +++ /dev/null @@ -1,392 +0,0 @@ -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { Dialog } from "@opencode-ai/ui/dialog" -import { FileIcon } from "@opencode-ai/ui/file-icon" -import { List } from "@opencode-ai/ui/list" -import type { ListRef } from "@opencode-ai/ui/list" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" -import fuzzysort from "fuzzysort" -import { createMemo, createResource, createSignal } from "solid-js" -import { useGlobalSDK } from "@/context/global-sdk" -import { useGlobalSync } from "@/context/global-sync" -import { useLayout } from "@/context/layout" -import { useLanguage } from "@/context/language" - -interface DialogSelectDirectoryProps { - title?: string - multiple?: boolean - onSelect: (result: string | string[] | null) => void -} - -type Row = { - absolute: string - search: string - group: "recent" | "folders" -} - -function cleanInput(value: string) { - const first = (value ?? "").split(/\r?\n/)[0] ?? "" - return first.replace(/[\u0000-\u001F\u007F]/g, "").trim() -} - -function normalizePath(input: string) { - const v = input.replaceAll("\\", "/") - if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/") - return v.replace(/\/+/g, "/") -} - -function normalizeDriveRoot(input: string) { - const v = normalizePath(input) - if (/^[A-Za-z]:$/.test(v)) return v + "/" - return v -} - -function trimTrailing(input: string) { - const v = normalizeDriveRoot(input) - if (v === "/") return v - if (v === "//") return v - if (/^[A-Za-z]:\/$/.test(v)) return v - return v.replace(/\/+$/, "") -} - -function joinPath(base: string | undefined, rel: string) { - const b = trimTrailing(base ?? "") - const r = trimTrailing(rel).replace(/^\/+/, "") - if (!b) return r - if (!r) return b - if (b.endsWith("/")) return b + r - return b + "/" + r -} - -function rootOf(input: string) { - const v = normalizeDriveRoot(input) - if (v.startsWith("//")) return "//" - if (v.startsWith("/")) return "/" - if (/^[A-Za-z]:\//.test(v)) return v.slice(0, 3) - return "" -} - -function parentOf(input: string) { - const v = trimTrailing(input) - if (v === "/") return v - if (v === "//") return v - if (/^[A-Za-z]:\/$/.test(v)) return v - - const i = v.lastIndexOf("/") - if (i <= 0) return "/" - if (i === 2 && /^[A-Za-z]:/.test(v)) return v.slice(0, 3) - return v.slice(0, i) -} - -function modeOf(input: string) { - const raw = normalizeDriveRoot(input.trim()) - if (!raw) return "relative" as const - if (raw.startsWith("~")) return "tilde" as const - if (rootOf(raw)) return "absolute" as const - return "relative" as const -} - -function tildeOf(absolute: string, home: string) { - const full = trimTrailing(absolute) - if (!home) return "" - - const hn = trimTrailing(home) - const lc = full.toLowerCase() - const hc = hn.toLowerCase() - if (lc === hc) return "~" - if (lc.startsWith(hc + "/")) return "~" + full.slice(hn.length) - return "" -} - -function displayPath(path: string, input: string, home: string) { - const full = trimTrailing(path) - if (modeOf(input) === "absolute") return full - return tildeOf(full, home) || full -} - -function toRow(absolute: string, home: string, group: Row["group"]): Row { - const full = trimTrailing(absolute) - const tilde = tildeOf(full, home) - const withSlash = (value: string) => { - if (!value) return "" - if (value.endsWith("/")) return value - return value + "/" - } - - const search = Array.from( - new Set([full, withSlash(full), tilde, withSlash(tilde), getFilename(full)].filter(Boolean)), - ).join("\n") - return { absolute: full, search, group } -} - -function uniqueRows(rows: Row[]) { - const seen = new Set() - return rows.filter((row) => { - if (seen.has(row.absolute)) return false - seen.add(row.absolute) - return true - }) -} - -function useDirectorySearch(args: { - sdk: ReturnType - start: () => string | undefined - home: () => string -}) { - const cache = new Map>>() - let current = 0 - - const scoped = (value: string) => { - const base = args.start() - if (!base) return - - const raw = normalizeDriveRoot(value) - if (!raw) return { directory: trimTrailing(base), path: "" } - - const h = args.home() - if (raw === "~") return { directory: trimTrailing(h || base), path: "" } - if (raw.startsWith("~/")) return { directory: trimTrailing(h || base), path: raw.slice(2) } - - const root = rootOf(raw) - if (root) return { directory: trimTrailing(root), path: raw.slice(root.length) } - return { directory: trimTrailing(base), path: raw } - } - - const dirs = async (dir: string) => { - const key = trimTrailing(dir) - const existing = cache.get(key) - if (existing) return existing - - const request = args.sdk.client.file - .list({ directory: key, path: "" }) - .then((x) => x.data ?? []) - .catch(() => []) - .then((nodes) => - nodes - .filter((n) => n.type === "directory") - .map((n) => ({ - name: n.name, - absolute: trimTrailing(normalizeDriveRoot(n.absolute)), - })), - ) - - cache.set(key, request) - return request - } - - const match = async (dir: string, query: string, limit: number) => { - const items = await dirs(dir) - if (!query) return items.slice(0, limit).map((x) => x.absolute) - return fuzzysort.go(query, items, { key: "name", limit }).map((x) => x.obj.absolute) - } - - return async (filter: string) => { - const token = ++current - const active = () => token === current - - const value = cleanInput(filter) - const scopedInput = scoped(value) - if (!scopedInput) return [] as string[] - - const raw = normalizeDriveRoot(value) - const isPath = raw.startsWith("~") || !!rootOf(raw) || raw.includes("/") - const query = normalizeDriveRoot(scopedInput.path) - - const find = () => - args.sdk.client.find - .files({ directory: scopedInput.directory, query, type: "directory", limit: 50 }) - .then((x) => x.data ?? []) - .catch(() => []) - - if (!isPath) { - const results = await find() - if (!active()) return [] - return results.map((rel) => joinPath(scopedInput.directory, rel)).slice(0, 50) - } - - const segments = query.replace(/^\/+/, "").split("/") - const head = segments.slice(0, segments.length - 1).filter((x) => x && x !== ".") - const tail = segments[segments.length - 1] ?? "" - - const cap = 12 - const branch = 4 - let paths = [scopedInput.directory] - for (const part of head) { - if (!active()) return [] - if (part === "..") { - paths = paths.map(parentOf) - continue - } - - const next = (await Promise.all(paths.map((p) => match(p, part, branch)))).flat() - if (!active()) return [] - paths = Array.from(new Set(next)).slice(0, cap) - if (paths.length === 0) return [] as string[] - } - - const out = (await Promise.all(paths.map((p) => match(p, tail, 50)))).flat() - if (!active()) return [] - const deduped = Array.from(new Set(out)) - const base = raw.startsWith("~") ? trimTrailing(scopedInput.directory) : "" - const expand = !raw.endsWith("/") - if (!expand || !tail) { - const items = base ? Array.from(new Set([base, ...deduped])) : deduped - return items.slice(0, 50) - } - - const needle = tail.toLowerCase() - const exact = deduped.filter((p) => getFilename(p).toLowerCase() === needle) - const target = exact[0] - if (!target) return deduped.slice(0, 50) - - const children = await match(target, "", 30) - if (!active()) return [] - const items = Array.from(new Set([...deduped, ...children])) - return (base ? Array.from(new Set([base, ...items])) : items).slice(0, 50) - } -} - -export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { - const sync = useGlobalSync() - const sdk = useGlobalSDK() - const layout = useLayout() - const dialog = useDialog() - const language = useLanguage() - - const [filter, setFilter] = createSignal("") - let list: ListRef | undefined - - const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory)) - const [fallbackPath] = createResource( - () => (missingBase() ? true : undefined), - async () => { - return sdk.client.path - .get() - .then((x) => x.data) - .catch(() => undefined) - }, - { initialValue: undefined }, - ) - - const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "") - const start = createMemo( - () => sync.data.path.home || sync.data.path.directory || fallbackPath()?.home || fallbackPath()?.directory, - ) - - const directories = useDirectorySearch({ - sdk, - home, - start, - }) - - const recentProjects = createMemo(() => { - const projects = layout.projects.list() - const byProject = new Map() - - for (const project of projects) { - let at = 0 - const dirs = [project.worktree, ...(project.sandboxes ?? [])] - for (const directory of dirs) { - const sessions = sync.child(directory, { bootstrap: false })[0].session - for (const session of sessions) { - if (session.time.archived) continue - const updated = session.time.updated ?? session.time.created - if (updated > at) at = updated - } - } - byProject.set(project.worktree, at) - } - - return projects - .map((project, index) => ({ project, at: byProject.get(project.worktree) ?? 0, index })) - .sort((a, b) => b.at - a.at || a.index - b.index) - .slice(0, 5) - .map(({ project }) => { - const row = toRow(project.worktree, home(), "recent") - const name = project.name || getFilename(project.worktree) - return { - ...row, - search: `${row.search}\n${name}`, - } - }) - }) - - const items = async (value: string) => { - const results = await directories(value) - const directoryRows = results.map((absolute) => toRow(absolute, home(), "folders")) - return uniqueRows([...recentProjects(), ...directoryRows]) - } - - function resolve(absolute: string) { - props.onSelect(props.multiple ? [absolute] : absolute) - dialog.close() - } - - return ( - - x.absolute} - filterKeys={["search"]} - groupBy={(item) => item.group} - sortGroupsBy={(a, b) => { - if (a.category === b.category) return 0 - return a.category === "recent" ? -1 : 1 - }} - groupHeader={(group) => - group.category === "recent" ? language.t("home.recentProjects") : language.t("command.project.open") - } - ref={(r) => (list = r)} - onFilter={(value) => setFilter(cleanInput(value))} - onKeyEvent={(e, item) => { - if (e.key !== "Tab") return - if (e.shiftKey) return - if (!item) return - - e.preventDefault() - e.stopPropagation() - - const value = displayPath(item.absolute, filter(), home()) - list?.setFilter(value.endsWith("/") ? value : value + "/") - }} - onSelect={(path) => { - if (!path) return - resolve(path.absolute) - }} - > - {(item) => { - const path = displayPath(item.absolute, filter(), home()) - if (path === "~") { - return ( -
-
- -
- ~ - / -
-
-
- ) - } - return ( -
-
- -
- - {getDirectory(path)} - - {getFilename(path)} - / -
-
-
- ) - }} -
-
- ) -} diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx deleted file mode 100644 index 186906f9204..00000000000 --- a/packages/app/src/components/dialog-select-file.tsx +++ /dev/null @@ -1,466 +0,0 @@ -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { Dialog } from "@opencode-ai/ui/dialog" -import { FileIcon } from "@opencode-ai/ui/file-icon" -import { Icon } from "@opencode-ai/ui/icon" -import { Keybind } from "@opencode-ai/ui/keybind" -import { List } from "@opencode-ai/ui/list" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" -import { useNavigate } from "@solidjs/router" -import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js" -import { formatKeybind, useCommand, type CommandOption } from "@/context/command" -import { useGlobalSDK } from "@/context/global-sdk" -import { useGlobalSync } from "@/context/global-sync" -import { useLayout } from "@/context/layout" -import { useFile } from "@/context/file" -import { useLanguage } from "@/context/language" -import { useSessionLayout } from "@/pages/session/session-layout" -import { createSessionTabs } from "@/pages/session/helpers" -import { decode64 } from "@/utils/base64" -import { getRelativeTime } from "@/utils/time" - -type EntryType = "command" | "file" | "session" - -type Entry = { - id: string - type: EntryType - title: string - description?: string - keybind?: string - category: string - option?: CommandOption - path?: string - directory?: string - sessionID?: string - archived?: number - updated?: number -} - -type DialogSelectFileMode = "all" | "files" - -const ENTRY_LIMIT = 5 -const COMMON_COMMAND_IDS = [ - "session.new", - "workspace.new", - "session.previous", - "session.next", - "terminal.toggle", - "review.toggle", -] as const - -const uniqueEntries = (items: Entry[]) => { - const seen = new Set() - const out: Entry[] = [] - for (const item of items) { - if (seen.has(item.id)) continue - seen.add(item.id) - out.push(item) - } - return out -} - -const createCommandEntry = (option: CommandOption, category: string): Entry => ({ - id: "command:" + option.id, - type: "command", - title: option.title, - description: option.description, - keybind: option.keybind, - category, - option, -}) - -const createFileEntry = (path: string, category: string): Entry => ({ - id: "file:" + path, - type: "file", - title: path, - category, - path, -}) - -const createSessionEntry = ( - input: { - directory: string - id: string - title: string - description: string - archived?: number - updated?: number - }, - category: string, -): Entry => ({ - id: `session:${input.directory}:${input.id}`, - type: "session", - title: input.title, - description: input.description, - category, - directory: input.directory, - sessionID: input.id, - archived: input.archived, - updated: input.updated, -}) - -function createCommandEntries(props: { - filesOnly: () => boolean - command: ReturnType - language: ReturnType -}) { - const allowed = createMemo(() => { - if (props.filesOnly()) return [] - return props.command.options.filter( - (option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open", - ) - }) - - const list = createMemo(() => { - const category = props.language.t("palette.group.commands") - return allowed().map((option) => createCommandEntry(option, category)) - }) - - const picks = createMemo(() => { - const all = allowed() - const order = new Map(COMMON_COMMAND_IDS.map((id, index) => [id, index])) - const picked = all.filter((option) => order.has(option.id)) - const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT) - const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base - const category = props.language.t("palette.group.commands") - return sorted.map((option) => createCommandEntry(option, category)) - }) - - return { allowed, list, picks } -} - -function createFileEntries(props: { - file: ReturnType - tabs: () => ReturnType["tabs"]> - language: ReturnType -}) { - const tabState = createSessionTabs({ - tabs: props.tabs, - pathFromTab: props.file.pathFromTab, - normalizeTab: (tab) => (tab.startsWith("file://") ? props.file.tab(tab) : tab), - }) - const recent = createMemo(() => { - const all = tabState.openedTabs() - const active = tabState.activeFileTab() - const order = active ? [active, ...all.filter((item) => item !== active)] : all - const seen = new Set() - const category = props.language.t("palette.group.files") - const items: Entry[] = [] - - for (const item of order) { - const path = props.file.pathFromTab(item) - if (!path) continue - if (seen.has(path)) continue - seen.add(path) - items.push(createFileEntry(path, category)) - } - - return items.slice(0, ENTRY_LIMIT) - }) - - const root = createMemo(() => { - const category = props.language.t("palette.group.files") - const nodes = props.file.tree.children("") - const paths = nodes - .filter((node) => node.type === "file") - .map((node) => node.path) - .sort((a, b) => a.localeCompare(b)) - return paths.slice(0, ENTRY_LIMIT).map((path) => createFileEntry(path, category)) - }) - - return { recent, root } -} - -function createSessionEntries(props: { - workspaces: () => string[] - label: (directory: string) => string - globalSDK: ReturnType - language: ReturnType -}) { - const state: { - token: number - inflight: Promise | undefined - cached: Entry[] | undefined - } = { - token: 0, - inflight: undefined, - cached: undefined, - } - - const sessions = (text: string) => { - const query = text.trim() - if (!query) { - state.token += 1 - state.inflight = undefined - state.cached = undefined - return [] as Entry[] - } - - if (state.cached) return state.cached - if (state.inflight) return state.inflight - - const current = state.token - const dirs = props.workspaces() - if (dirs.length === 0) return [] as Entry[] - - state.inflight = Promise.all( - dirs.map((directory) => { - const description = props.label(directory) - return props.globalSDK.client.session - .list({ directory, roots: true }) - .then((x) => - (x.data ?? []) - .filter((s) => !!s?.id) - .map((s) => ({ - id: s.id, - title: s.title ?? props.language.t("command.session.new"), - description, - directory, - archived: s.time?.archived, - updated: s.time?.updated, - })), - ) - .catch( - () => - [] as { - id: string - title: string - description: string - directory: string - archived?: number - updated?: number - }[], - ) - }), - ) - .then((results) => { - if (state.token !== current) return [] as Entry[] - const seen = new Set() - const category = props.language.t("command.category.session") - const next = results - .flat() - .filter((item) => { - const key = `${item.directory}:${item.id}` - if (seen.has(key)) return false - seen.add(key) - return true - }) - .map((item) => createSessionEntry(item, category)) - state.cached = next - return next - }) - .catch(() => [] as Entry[]) - .finally(() => { - state.inflight = undefined - }) - - return state.inflight - } - - return { sessions } -} - -export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) { - const command = useCommand() - const language = useLanguage() - const layout = useLayout() - const file = useFile() - const dialog = useDialog() - const navigate = useNavigate() - const globalSDK = useGlobalSDK() - const globalSync = useGlobalSync() - const { params, tabs, view } = useSessionLayout() - const filesOnly = () => props.mode === "files" - const state = { cleanup: undefined as (() => void) | void, committed: false } - const [grouped, setGrouped] = createSignal(false) - const commandEntries = createCommandEntries({ filesOnly, command, language }) - const fileEntries = createFileEntries({ file, tabs, language }) - - const projectDirectory = createMemo(() => decode64(params.dir) ?? "") - const project = createMemo(() => { - const directory = projectDirectory() - if (!directory) return - return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory)) - }) - const workspaces = createMemo(() => { - const directory = projectDirectory() - const current = project() - if (!current) return directory ? [directory] : [] - - const dirs = [current.worktree, ...(current.sandboxes ?? [])] - if (directory && !dirs.includes(directory)) return [...dirs, directory] - return dirs - }) - const homedir = createMemo(() => globalSync.data.path.home) - const label = (directory: string) => { - const current = project() - const kind = - current && directory === current.worktree - ? language.t("workspace.type.local") - : language.t("workspace.type.sandbox") - const [store] = globalSync.child(directory, { bootstrap: false }) - const home = homedir() - const path = home ? directory.replace(home, "~") : directory - const name = store.vcs?.branch ?? getFilename(directory) - return `${kind} : ${name || path}` - } - - const { sessions } = createSessionEntries({ workspaces, label, globalSDK, language }) - - const items = async (text: string) => { - const query = text.trim() - setGrouped(query.length > 0) - - if (!query && filesOnly()) { - const loaded = file.tree.state("")?.loaded - const pending = loaded ? Promise.resolve() : file.tree.list("") - const next = uniqueEntries([...fileEntries.recent(), ...fileEntries.root()]) - - if (loaded || next.length > 0) { - void pending - return next - } - - await pending - return uniqueEntries([...fileEntries.recent(), ...fileEntries.root()]) - } - - if (!query) return [...commandEntries.picks(), ...fileEntries.recent()] - - if (filesOnly()) { - const files = await file.searchFiles(query) - const category = language.t("palette.group.files") - return files.map((path) => createFileEntry(path, category)) - } - - const [files, nextSessions] = await Promise.all([file.searchFiles(query), Promise.resolve(sessions(query))]) - const category = language.t("palette.group.files") - const entries = files.map((path) => createFileEntry(path, category)) - return [...commandEntries.list(), ...nextSessions, ...entries] - } - - const handleMove = (item: Entry | undefined) => { - state.cleanup?.() - if (!item) return - if (item.type !== "command") return - state.cleanup = item.option?.onHighlight?.() - } - - const open = (path: string) => { - const value = file.tab(path) - void tabs().open(value) - void file.load(path) - if (!view().reviewPanel.opened()) view().reviewPanel.open() - layout.fileTree.setTab("all") - props.onOpenFile?.(path) - tabs().setActive(value) - } - - const handleSelect = (item: Entry | undefined) => { - if (!item) return - state.committed = true - state.cleanup = undefined - dialog.close() - - if (item.type === "command") { - item.option?.onSelect?.("palette") - return - } - - if (item.type === "session") { - if (!item.directory || !item.sessionID) return - navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`) - return - } - - if (!item.path) return - open(item.path) - } - - onCleanup(() => { - if (state.committed) return - state.cleanup?.() - }) - - return ( - - item.id} - filterKeys={["title", "description", "category"]} - groupBy={grouped() ? (item) => item.category : () => ""} - onMove={handleMove} - onSelect={handleSelect} - > - {(item) => ( - -
- -
- - {getDirectory(item.path ?? "")} - - {getFilename(item.path ?? "")} -
-
-
- } - > - -
-
- {item.title} - - {item.description} - -
- - {formatKeybind(item.keybind ?? "", language.t)} - -
-
- -
-
- -
- - {item.title} - - - - {item.description} - - -
-
- - - {getRelativeTime(new Date(item.updated!).toISOString(), language.t)} - - -
-
-
- )} - - - ) -} diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx deleted file mode 100644 index 98f262ce5a3..00000000000 --- a/packages/app/src/components/dialog-select-mcp.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { useMutation } from "@tanstack/solid-query" -import { Component, createEffect, createMemo, on, Show } from "solid-js" -import { createStore } from "solid-js/store" -import { useSync } from "@/context/sync" -import { useSDK } from "@/context/sdk" -import { Dialog } from "@opencode-ai/ui/dialog" -import { List } from "@opencode-ai/ui/list" -import { Switch } from "@opencode-ai/ui/switch" -import { showToast } from "@opencode-ai/ui/toast" -import { useLanguage } from "@/context/language" - -const statusLabels = { - connected: "mcp.status.connected", - failed: "mcp.status.failed", - needs_auth: "mcp.status.needs_auth", - disabled: "mcp.status.disabled", -} as const - -export const DialogSelectMcp: Component = () => { - const sync = useSync() - const sdk = useSDK() - const language = useLanguage() - const [state, setState] = createStore({ - done: false, - loading: false, - }) - - createEffect( - on( - () => sync.data.mcp_ready, - (ready, prev) => { - if (!ready && prev) setState("done", false) - }, - { defer: true }, - ), - ) - - createEffect(() => { - if (state.done || state.loading) return - if (sync.data.mcp_ready) { - setState("done", true) - return - } - - setState("loading", true) - void sdk.client.mcp - .status() - .then((result) => { - sync.set("mcp", result.data ?? {}) - sync.set("mcp_ready", true) - setState("done", true) - }) - .catch((err) => { - setState("done", true) - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: err instanceof Error ? err.message : String(err), - }) - }) - .finally(() => { - setState("loading", false) - }) - }) - - const items = createMemo(() => - Object.entries(sync.data.mcp ?? {}) - .map(([name, status]) => ({ name, status: status.status })) - .sort((a, b) => a.name.localeCompare(b.name)), - ) - - const toggle = useMutation(() => ({ - mutationFn: async (name: string) => { - const status = sync.data.mcp[name] - if (status?.status === "connected") { - await sdk.client.mcp.disconnect({ name }) - } else { - await sdk.client.mcp.connect({ name }) - } - - const result = await sdk.client.mcp.status() - if (result.data) sync.set("mcp", result.data) - }, - })) - - const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length) - const totalCount = createMemo(() => items().length) - - return ( - - x?.name ?? ""} - items={items} - filterKeys={["name", "status"]} - sortBy={(a, b) => a.name.localeCompare(b.name)} - onSelect={(x) => { - if (!x || toggle.isPending) return - toggle.mutate(x.name) - }} - > - {(i) => { - const mcpStatus = () => sync.data.mcp[i.name] - const status = () => mcpStatus()?.status - const statusLabel = () => { - const key = status() ? statusLabels[status() as keyof typeof statusLabels] : undefined - if (!key) return - return language.t(key) - } - const error = () => { - const s = mcpStatus() - return s?.status === "failed" ? s.error : undefined - } - const enabled = () => status() === "connected" - return ( -
-
-
- {i.name} - - {statusLabel()} - - - {language.t("common.loading.ellipsis")} - -
- - {error()} - -
-
e.stopPropagation()}> - { - if (toggle.isPending) return - toggle.mutate(i.name) - }} - /> -
-
- ) - }} -
-
- ) -} diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx deleted file mode 100644 index e25e8f0c17d..00000000000 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { Button } from "@opencode-ai/ui/button" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { Dialog } from "@opencode-ai/ui/dialog" -import { List, type ListRef } from "@opencode-ai/ui/list" -import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { Tag } from "@opencode-ai/ui/tag" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { type Component, Show } from "solid-js" -import { useLocal } from "@/context/local" -import { popularProviders, useProviders } from "@/hooks/use-providers" -import { ModelTooltip } from "./model-tooltip" -import { useLanguage } from "@/context/language" - -type ModelState = ReturnType["model"] - -export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props) => { - const model = props.model ?? useLocal().model - const dialog = useDialog() - const providers = useProviders() - const language = useLanguage() - - const connect = (provider: string) => { - void import("./dialog-connect-provider").then((x) => { - dialog.show(() => ) - }) - } - - const all = () => { - void import("./dialog-select-provider").then((x) => { - dialog.show(() => ) - }) - } - - let listRef: ListRef | undefined - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") return - listRef?.onKeyDown(e) - } - - return ( - -
-
{language.t("dialog.model.unpaid.freeModels.title")}
- (listRef = ref)} - items={model.list} - current={model.current()} - key={(x) => `${x.provider.id}:${x.id}`} - itemWrapper={(item, node) => ( - - } - > - {node} - - )} - onSelect={(x) => { - model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { - recent: true, - }) - dialog.close() - }} - > - {(i) => ( -
- {i.name} - {language.t("model.tag.free")} - - {language.t("model.tag.latest")} - -
- )} -
-
-
-
-
-
{language.t("dialog.model.unpaid.addMore.title")}
-
- x?.id} - items={providers.popular} - activeIcon="plus-small" - sortBy={(a, b) => { - if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) - return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) - return a.name.localeCompare(b.name) - }} - onSelect={(x) => { - if (!x) return - connect(x.id) - }} - > - {(i) => ( -
- - {i.name} - -
{language.t("dialog.provider.opencode.tagline")}
-
- - {language.t("dialog.provider.tag.recommended")} - - - <> -
- {language.t("dialog.provider.opencodeGo.tagline")} -
- {language.t("dialog.provider.tag.recommended")} - -
- -
{language.t("dialog.provider.anthropic.note")}
-
-
- )} -
- -
-
-
-
-
- ) -} diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx deleted file mode 100644 index e9c99e08fb6..00000000000 --- a/packages/app/src/components/dialog-select-model.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { Popover as Kobalte } from "@kobalte/core/popover" -import { Component, ComponentProps, createMemo, JSX, Show, ValidComponent } from "solid-js" -import { createStore } from "solid-js/store" -import { useLocal } from "@/context/local" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { popularProviders } from "@/hooks/use-providers" -import { Button } from "@opencode-ai/ui/button" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Tag } from "@opencode-ai/ui/tag" -import { Dialog } from "@opencode-ai/ui/dialog" -import { List } from "@opencode-ai/ui/list" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { ModelTooltip } from "./model-tooltip" -import { useLanguage } from "@/context/language" - -const isFree = (provider: string, cost: { input: number } | undefined) => - provider === "opencode" && (!cost || cost.input === 0) - -type ModelState = ReturnType["model"] - -const ModelList: Component<{ - provider?: string - class?: string - onSelect: () => void - action?: JSX.Element - model?: ModelState -}> = (props) => { - const model = props.model ?? useLocal().model - const language = useLanguage() - - const models = createMemo(() => - model - .list() - .filter((m) => model.visible({ modelID: m.id, providerID: m.provider.id })) - .filter((m) => (props.provider ? m.provider.id === props.provider : true)), - ) - - return ( - `${x.provider.id}:${x.id}`} - items={models} - current={model.current()} - filterKeys={["provider.name", "name", "id"]} - sortBy={(a, b) => a.name.localeCompare(b.name)} - groupBy={(x) => x.provider.name} - sortGroupsBy={(a, b) => { - const aProvider = a.items[0].provider.id - const bProvider = b.items[0].provider.id - if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1 - if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1 - return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider) - }} - itemWrapper={(item, node) => ( - } - > - {node} - - )} - onSelect={(x) => { - model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { - recent: true, - }) - props.onSelect() - }} - > - {(i) => ( -
- {i.name} - - {language.t("model.tag.free")} - - - {language.t("model.tag.latest")} - -
- )} -
- ) -} - -type ModelSelectorTriggerProps = Omit, "as" | "ref"> -type Dismiss = "escape" | "outside" | "select" | "manage" | "provider" - -export function ModelSelectorPopover(props: { - provider?: string - model?: ModelState - children?: JSX.Element - triggerAs?: ValidComponent - triggerProps?: ModelSelectorTriggerProps - onClose?: (cause: "escape" | "select") => void -}) { - const [store, setStore] = createStore<{ - open: boolean - dismiss: Dismiss | null - }>({ - open: false, - dismiss: null, - }) - const dialog = useDialog() - - const close = (dismiss: Dismiss) => { - setStore("dismiss", dismiss) - setStore("open", false) - } - - const handleManage = () => { - close("manage") - void import("./dialog-manage-models").then((x) => { - dialog.show(() => ) - }) - } - - const handleConnectProvider = () => { - close("provider") - void import("./dialog-select-provider").then((x) => { - dialog.show(() => ) - }) - } - const language = useLanguage() - - return ( - { - if (next) setStore("dismiss", null) - setStore("open", next) - }} - modal={false} - placement="top-start" - gutter={4} - > - - {props.children} - - - { - close("escape") - event.preventDefault() - event.stopPropagation() - }} - onPointerDownOutside={() => close("outside")} - onFocusOutside={() => close("outside")} - onCloseAutoFocus={(event) => { - const dismiss = store.dismiss - if (dismiss === "outside") event.preventDefault() - if (dismiss === "escape" || dismiss === "select") { - event.preventDefault() - props.onClose?.(dismiss) - } - setStore("dismiss", null) - }} - > - {language.t("dialog.model.select.title")} - close("select")} - class="p-1" - action={ -
- - - - - - -
- } - /> -
-
-
- ) -} - -export const DialogSelectModel: Component<{ provider?: string; model?: ModelState }> = (props) => { - const dialog = useDialog() - const language = useLanguage() - - const provider = () => { - void import("./dialog-select-provider").then((x) => { - dialog.show(() => ) - }) - } - - const manage = () => { - void import("./dialog-manage-models").then((x) => { - dialog.show(() => ) - }) - } - - return ( - - {language.t("command.provider.connect")} - - } - > - dialog.close()} /> - - - ) -} diff --git a/packages/app/src/components/dialog-select-provider.tsx b/packages/app/src/components/dialog-select-provider.tsx deleted file mode 100644 index 9dcf81e1f0d..00000000000 --- a/packages/app/src/components/dialog-select-provider.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { Component, Show } from "solid-js" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { popularProviders, useProviders } from "@/hooks/use-providers" -import { Dialog } from "@opencode-ai/ui/dialog" -import { List } from "@opencode-ai/ui/list" -import { Tag } from "@opencode-ai/ui/tag" -import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { DialogConnectProvider } from "./dialog-connect-provider" -import { useLanguage } from "@/context/language" -import { DialogCustomProvider } from "./dialog-custom-provider" - -const CUSTOM_ID = "_custom" - -export const DialogSelectProvider: Component = () => { - const dialog = useDialog() - const providers = useProviders() - const language = useLanguage() - - // kilocode_change start - Use "Recommended" terminology to match kilocode - const popularGroup = () => language.t("dialog.provider.group.recommended") - // kilocode_change end - const otherGroup = () => language.t("dialog.provider.group.other") - const customLabel = () => language.t("settings.providers.tag.custom") - const note = (id: string) => { - if (id === "anthropic") return language.t("dialog.provider.anthropic.note") - if (id === "openai") return language.t("dialog.provider.openai.note") - if (id.startsWith("github-copilot")) return language.t("dialog.provider.copilot.note") - if (id === "opencode-go") return language.t("dialog.provider.opencodeGo.tagline") - } - - return ( - - x?.id} - items={() => { - language.locale() - return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all()] - }} - filterKeys={["id", "name"]} - groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())} - sortBy={(a, b) => { - if (a.id === CUSTOM_ID) return -1 - if (b.id === CUSTOM_ID) return 1 - if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) - return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) - return a.name.localeCompare(b.name) - }} - sortGroupsBy={(a, b) => { - const popular = popularGroup() - if (a.category === popular && b.category !== popular) return -1 - if (b.category === popular && a.category !== popular) return 1 - return 0 - }} - onSelect={(x) => { - if (!x) return - if (x.id === CUSTOM_ID) { - dialog.show(() => ) - return - } - dialog.show(() => ) - }} - > - {(i) => ( -
- - {i.name} - {/* kilocode_change start - Provider tags and notes */} - - {language.t("dialog.provider.tag.recommended")} -
{language.t("dialog.provider.kilo.note")}
-
- {/* kilocode_change end */} - - {language.t("settings.providers.tag.custom")} - - - {language.t("dialog.provider.tag.recommended")} - - {(value) =>
{value()}
}
- - {language.t("dialog.provider.tag.recommended")} - -
- )} -
-
- ) -} diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx deleted file mode 100644 index 0cb5a2d6046..00000000000 --- a/packages/app/src/components/dialog-select-server.tsx +++ /dev/null @@ -1,649 +0,0 @@ -import { Button } from "@opencode-ai/ui/button" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { Dialog } from "@opencode-ai/ui/dialog" -import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" -import { Icon } from "@opencode-ai/ui/icon" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { List } from "@opencode-ai/ui/list" -import { TextField } from "@opencode-ai/ui/text-field" -import { useMutation } from "@tanstack/solid-query" -import { showToast } from "@opencode-ai/ui/toast" -import { useNavigate } from "@solidjs/router" -import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js" -import { createStore, reconcile } from "solid-js/store" -import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row" -import { useLanguage } from "@/context/language" -import { usePlatform } from "@/context/platform" -import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" -import { type ServerHealth, useCheckServerHealth } from "@/utils/server-health" - -const DEFAULT_USERNAME = "opencode" - -interface ServerFormProps { - value: string - name: string - username: string - password: string - placeholder: string - busy: boolean - error: string - status: boolean | undefined - onChange: (value: string) => void - onNameChange: (value: string) => void - onUsernameChange: (value: string) => void - onPasswordChange: (value: string) => void - onSubmit: () => void - onBack: () => void -} - -function showRequestError(language: ReturnType, err: unknown) { - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: err instanceof Error ? err.message : String(err), - }) -} - -function useDefaultServer() { - const language = useLanguage() - const platform = usePlatform() - const [defaultKey, defaultUrlActions] = createResource( - async () => { - try { - const key = await platform.getDefaultServer?.() - if (!key) return null - return key - } catch (err) { - showRequestError(language, err) - return null - } - }, - { initialValue: null }, - ) - - const canDefault = createMemo(() => !!platform.getDefaultServer && !!platform.setDefaultServer) - const setDefault = async (key: ServerConnection.Key | null) => { - try { - await platform.setDefaultServer?.(key) - defaultUrlActions.mutate(key) - } catch (err) { - showRequestError(language, err) - } - } - - return { defaultKey, canDefault, setDefault } -} - -function useServerPreview() { - const checkServerHealth = useCheckServerHealth() - - const looksComplete = (value: string) => { - const normalized = normalizeServerUrl(value) - if (!normalized) return false - const host = normalized.replace(/^https?:\/\//, "").split("/")[0] - if (!host) return false - if (host.includes("localhost") || host.startsWith("127.0.0.1")) return true - return host.includes(".") || host.includes(":") - } - - const previewStatus = async ( - value: string, - username: string, - password: string, - setStatus: (value: boolean | undefined) => void, - ) => { - setStatus(undefined) - if (!looksComplete(value)) return - const normalized = normalizeServerUrl(value) - if (!normalized) return - const http: ServerConnection.HttpBase = { url: normalized } - if (username) http.username = username - if (password) http.password = password - const result = await checkServerHealth(http) - setStatus(result.healthy) - } - - return { previewStatus } -} - -function ServerForm(props: ServerFormProps) { - const language = useLanguage() - const keyDown = (event: KeyboardEvent) => { - event.stopPropagation() - if (event.key === "Escape") { - event.preventDefault() - props.onBack() - return - } - if (event.key !== "Enter" || event.isComposing) return - event.preventDefault() - props.onSubmit() - } - - return ( -
-
-
- -
- -
- - -
-
-
- ) -} - -export function DialogSelectServer() { - const navigate = useNavigate() - const dialog = useDialog() - const server = useServer() - const platform = usePlatform() - const language = useLanguage() - const { defaultKey, canDefault, setDefault } = useDefaultServer() - const { previewStatus } = useServerPreview() - const checkServerHealth = useCheckServerHealth() - const [store, setStore] = createStore({ - status: {} as Record, - addServer: { - url: "", - name: "", - username: DEFAULT_USERNAME, - password: "", - error: "", - showForm: false, - status: undefined as boolean | undefined, - }, - editServer: { - id: undefined as string | undefined, - value: "", - name: "", - username: "", - password: "", - error: "", - status: undefined as boolean | undefined, - }, - }) - - const resetAdd = () => { - setStore("addServer", { - url: "", - name: "", - username: DEFAULT_USERNAME, - password: "", - error: "", - showForm: false, - status: undefined, - }) - } - const resetEdit = () => { - setStore("editServer", { - id: undefined, - value: "", - name: "", - username: "", - password: "", - error: "", - status: undefined, - }) - } - - const addMutation = useMutation(() => ({ - mutationFn: async (value: string) => { - const normalized = normalizeServerUrl(value) - if (!normalized) { - resetAdd() - return - } - - const conn: ServerConnection.Http = { - type: "http", - http: { url: normalized }, - } - if (store.addServer.name.trim()) conn.displayName = store.addServer.name.trim() - if (store.addServer.password) conn.http.password = store.addServer.password - if (store.addServer.password && store.addServer.username) conn.http.username = store.addServer.username - const result = await checkServerHealth(conn.http) - if (!result.healthy) { - setStore("addServer", { error: language.t("dialog.server.add.error") }) - return - } - - resetAdd() - await select(conn, true) - }, - })) - - const editMutation = useMutation(() => ({ - mutationFn: async (input: { original: ServerConnection.Any; value: string }) => { - if (input.original.type !== "http") return - const normalized = normalizeServerUrl(input.value) - if (!normalized) { - resetEdit() - return - } - - const name = store.editServer.name.trim() || undefined - const username = store.editServer.username || undefined - const password = store.editServer.password || undefined - const existingName = input.original.displayName - if ( - normalized === input.original.http.url && - name === existingName && - username === input.original.http.username && - password === input.original.http.password - ) { - resetEdit() - return - } - - const conn: ServerConnection.Http = { - type: "http", - displayName: name, - http: { url: normalized, username, password }, - } - const result = await checkServerHealth(conn.http) - if (!result.healthy) { - setStore("editServer", { error: language.t("dialog.server.add.error") }) - return - } - if (normalized === input.original.http.url) { - server.add(conn) - } else { - replaceServer(input.original, conn) - } - - resetEdit() - }, - })) - - const replaceServer = (original: ServerConnection.Http, next: ServerConnection.Http) => { - const active = server.key - const newConn = server.add(next) - if (!newConn) return - const nextActive = active === ServerConnection.key(original) ? ServerConnection.key(newConn) : active - if (nextActive) server.setActive(nextActive) - server.remove(ServerConnection.key(original)) - } - - const items = createMemo(() => { - const current = server.current - const list = server.list - if (!current) return list - if (!list.includes(current)) return [current, ...list] - return [current, ...list.filter((x) => x !== current)] - }) - - const current = createMemo(() => items().find((x) => ServerConnection.key(x) === server.key) ?? items()[0]) - - const sortedItems = createMemo(() => { - const list = items() - if (!list.length) return list - const active = current() - const order = new Map(list.map((url, index) => [url, index] as const)) - const rank = (value?: ServerHealth) => { - if (value?.healthy === true) return 0 - if (value?.healthy === false) return 2 - return 1 - } - return list.slice().sort((a, b) => { - if (a === active) return -1 - if (b === active) return 1 - const diff = rank(store.status[ServerConnection.key(a)]) - rank(store.status[ServerConnection.key(b)]) - if (diff !== 0) return diff - return (order.get(a) ?? 0) - (order.get(b) ?? 0) - }) - }) - - async function refreshHealth() { - const results: Record = {} - await Promise.all( - items().map(async (conn) => { - results[ServerConnection.key(conn)] = await checkServerHealth(conn.http) - }), - ) - setStore("status", reconcile(results)) - } - - createEffect(() => { - items() - void refreshHealth() - const interval = setInterval(refreshHealth, 10_000) - onCleanup(() => clearInterval(interval)) - }) - - async function select(conn: ServerConnection.Any, persist?: boolean) { - if (!persist && store.status[ServerConnection.key(conn)]?.healthy === false) return - dialog.close() - if (persist && conn.type === "http") { - server.add(conn) - navigate("/") - return - } - navigate("/") - queueMicrotask(() => server.setActive(ServerConnection.key(conn))) - } - - const handleAddChange = (value: string) => { - if (addMutation.isPending) return - setStore("addServer", { url: value, error: "" }) - void previewStatus(value, store.addServer.username, store.addServer.password, (next) => - setStore("addServer", { status: next }), - ) - } - - const handleAddNameChange = (value: string) => { - if (addMutation.isPending) return - setStore("addServer", { name: value, error: "" }) - } - - const handleAddUsernameChange = (value: string) => { - if (addMutation.isPending) return - setStore("addServer", { username: value, error: "" }) - void previewStatus(store.addServer.url, value, store.addServer.password, (next) => - setStore("addServer", { status: next }), - ) - } - - const handleAddPasswordChange = (value: string) => { - if (addMutation.isPending) return - setStore("addServer", { password: value, error: "" }) - void previewStatus(store.addServer.url, store.addServer.username, value, (next) => - setStore("addServer", { status: next }), - ) - } - - const handleEditChange = (value: string) => { - if (editMutation.isPending) return - setStore("editServer", { value, error: "" }) - void previewStatus(value, store.editServer.username, store.editServer.password, (next) => - setStore("editServer", { status: next }), - ) - } - - const handleEditNameChange = (value: string) => { - if (editMutation.isPending) return - setStore("editServer", { name: value, error: "" }) - } - - const handleEditUsernameChange = (value: string) => { - if (editMutation.isPending) return - setStore("editServer", { username: value, error: "" }) - void previewStatus(store.editServer.value, value, store.editServer.password, (next) => - setStore("editServer", { status: next }), - ) - } - - const handleEditPasswordChange = (value: string) => { - if (editMutation.isPending) return - setStore("editServer", { password: value, error: "" }) - void previewStatus(store.editServer.value, store.editServer.username, value, (next) => - setStore("editServer", { status: next }), - ) - } - - const mode = createMemo<"list" | "add" | "edit">(() => { - if (store.editServer.id) return "edit" - if (store.addServer.showForm) return "add" - return "list" - }) - - const editing = createMemo(() => { - if (!store.editServer.id) return - return items().find((x) => x.type === "http" && x.http.url === store.editServer.id) - }) - - const resetForm = () => { - resetAdd() - resetEdit() - } - - const startAdd = () => { - resetEdit() - setStore("addServer", { - showForm: true, - url: "", - name: "", - username: DEFAULT_USERNAME, - password: "", - error: "", - status: undefined, - }) - } - - const startEdit = (conn: ServerConnection.Http) => { - resetAdd() - setStore("editServer", { - id: conn.http.url, - value: conn.http.url, - name: conn.displayName ?? "", - username: conn.http.username ?? "", - password: conn.http.password ?? "", - error: "", - status: store.status[ServerConnection.key(conn)]?.healthy, - }) - } - - const submitForm = () => { - if (mode() === "add") { - if (addMutation.isPending) return - setStore("addServer", { error: "" }) - addMutation.mutate(store.addServer.url) - return - } - const original = editing() - if (!original) return - if (editMutation.isPending) return - setStore("editServer", { error: "" }) - editMutation.mutate({ original, value: store.editServer.value }) - } - - const isFormMode = createMemo(() => mode() !== "list") - const isAddMode = createMemo(() => mode() === "add") - const formBusy = createMemo(() => (isAddMode() ? addMutation.isPending : editMutation.isPending)) - - const formTitle = createMemo(() => { - if (!isFormMode()) return language.t("dialog.server.title") - return ( -
- - {isAddMode() ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")} -
- ) - }) - - createEffect(() => { - if (!store.editServer.id) return - if (editing()) return - resetEdit() - }) - - async function handleRemove(url: ServerConnection.Key) { - server.remove(url) - if ((await platform.getDefaultServer?.()) === url) { - void platform.setDefaultServer?.(null) - } - } - - return ( - -
- - } - > - x.http.url} - onSelect={(x) => { - if (x) void select(x) - }} - divider={true} - class="flex-1 min-h-0 px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent" - > - {(i) => { - const key = ServerConnection.key(i) - return ( -
-
- -
- - - {language.t("dialog.server.status.default")} - - - } - showCredentials - /> -
- - - - - - - e.stopPropagation()} - onPointerDown={(e: PointerEvent) => e.stopPropagation()} - /> - - - { - if (i.type !== "http") return - startEdit(i) - }} - > - {language.t("dialog.server.menu.edit")} - - - setDefault(key)}> - - {language.t("dialog.server.menu.default")} - - - - - setDefault(null)}> - - {language.t("dialog.server.menu.defaultRemove")} - - - - - handleRemove(ServerConnection.key(i))} - class="text-text-on-critical-base hover:bg-surface-critical-weak" - > - {language.t("dialog.server.menu.delete")} - - - - - -
-
- ) - }} -
-
- -
- - {language.t("dialog.server.add.button")} - - } - > - - -
-
-
- ) -} diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx deleted file mode 100644 index 83cea131f5d..00000000000 --- a/packages/app/src/components/dialog-settings.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Component } from "solid-js" -import { Dialog } from "@opencode-ai/ui/dialog" -import { Tabs } from "@opencode-ai/ui/tabs" -import { Icon } from "@opencode-ai/ui/icon" -import { useLanguage } from "@/context/language" -import { usePlatform } from "@/context/platform" -import { SettingsGeneral } from "./settings-general" -import { SettingsKeybinds } from "./settings-keybinds" -import { SettingsProviders } from "./settings-providers" -import { SettingsModels } from "./settings-models" - -export const DialogSettings: Component = () => { - const language = useLanguage() - const platform = usePlatform() - - return ( - - - -
-
-
-
- {language.t("settings.section.desktop")} -
- - - {language.t("settings.tab.general")} - - - - {language.t("settings.tab.shortcuts")} - -
-
- -
- {language.t("settings.section.server")} -
- - - {language.t("settings.providers.title")} - - - - {language.t("settings.models.title")} - -
-
-
-
-
- {language.t("app.name.desktop")} - v{platform.version} -
-
-
- - - - - - - - - - - - -
-
- ) -} diff --git a/packages/app/src/components/file-tree.test.ts b/packages/app/src/components/file-tree.test.ts deleted file mode 100644 index 29e20b4807c..00000000000 --- a/packages/app/src/components/file-tree.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { beforeAll, describe, expect, mock, test } from "bun:test" - -let shouldListRoot: typeof import("./file-tree").shouldListRoot -let shouldListExpanded: typeof import("./file-tree").shouldListExpanded -let dirsToExpand: typeof import("./file-tree").dirsToExpand - -beforeAll(async () => { - mock.module("@solidjs/router", () => ({ - useNavigate: () => () => undefined, - useParams: () => ({}), - })) - mock.module("@/context/file", () => ({ - useFile: () => ({ - tree: { - state: () => undefined, - list: () => Promise.resolve(), - children: () => [], - expand: () => {}, - collapse: () => {}, - }, - }), - })) - mock.module("@opencode-ai/ui/collapsible", () => ({ - Collapsible: { - Trigger: (props: { children?: unknown }) => props.children, - Content: (props: { children?: unknown }) => props.children, - }, - })) - mock.module("@opencode-ai/ui/file-icon", () => ({ FileIcon: () => null })) - mock.module("@opencode-ai/ui/icon", () => ({ Icon: () => null })) - mock.module("@opencode-ai/ui/tooltip", () => ({ Tooltip: (props: { children?: unknown }) => props.children })) - const mod = await import("./file-tree") - shouldListRoot = mod.shouldListRoot - shouldListExpanded = mod.shouldListExpanded - dirsToExpand = mod.dirsToExpand -}) - -describe("file tree fetch discipline", () => { - test("root lists on mount unless already loaded or loading", () => { - expect(shouldListRoot({ level: 0 })).toBe(true) - expect(shouldListRoot({ level: 0, dir: { loaded: true } })).toBe(false) - expect(shouldListRoot({ level: 0, dir: { loading: true } })).toBe(false) - expect(shouldListRoot({ level: 1 })).toBe(false) - }) - - test("nested dirs list only when expanded and stale", () => { - expect(shouldListExpanded({ level: 1 })).toBe(false) - expect(shouldListExpanded({ level: 1, dir: { expanded: false } })).toBe(false) - expect(shouldListExpanded({ level: 1, dir: { expanded: true } })).toBe(true) - expect(shouldListExpanded({ level: 1, dir: { expanded: true, loaded: true } })).toBe(false) - expect(shouldListExpanded({ level: 1, dir: { expanded: true, loading: true } })).toBe(false) - expect(shouldListExpanded({ level: 0, dir: { expanded: true } })).toBe(false) - }) - - test("allowed auto-expand picks only collapsed dirs", () => { - const expanded = new Set() - const filter = { dirs: new Set(["src", "src/components"]) } - - const first = dirsToExpand({ - level: 0, - filter, - expanded: (dir) => expanded.has(dir), - }) - - expect(first).toEqual(["src", "src/components"]) - - for (const dir of first) expanded.add(dir) - - const second = dirsToExpand({ - level: 0, - filter, - expanded: (dir) => expanded.has(dir), - }) - - expect(second).toEqual([]) - expect(dirsToExpand({ level: 1, filter, expanded: () => false })).toEqual([]) - }) -}) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx deleted file mode 100644 index f4148dfe4ed..00000000000 --- a/packages/app/src/components/file-tree.tsx +++ /dev/null @@ -1,506 +0,0 @@ -import { useFile } from "@/context/file" -import { encodeFilePath } from "@/context/file/path" -import { Collapsible } from "@opencode-ai/ui/collapsible" -import { FileIcon } from "@opencode-ai/ui/file-icon" -import { Icon } from "@opencode-ai/ui/icon" -import { - createEffect, - createMemo, - For, - Match, - on, - Show, - splitProps, - Switch, - untrack, - type ComponentProps, - type ParentProps, -} from "solid-js" -import { Dynamic } from "solid-js/web" -import type { FileNode } from "@kilocode/sdk/v2" - -const MAX_DEPTH = 128 - -function pathToFileUrl(filepath: string): string { - return `file://${encodeFilePath(filepath)}` -} - -type Kind = "add" | "del" | "mix" - -type Filter = { - files: Set - dirs: Set -} - -export function shouldListRoot(input: { level: number; dir?: { loaded?: boolean; loading?: boolean } }) { - if (input.level !== 0) return false - if (input.dir?.loaded) return false - if (input.dir?.loading) return false - return true -} - -export function shouldListExpanded(input: { - level: number - dir?: { expanded?: boolean; loaded?: boolean; loading?: boolean } -}) { - if (input.level === 0) return false - if (!input.dir?.expanded) return false - if (input.dir.loaded) return false - if (input.dir.loading) return false - return true -} - -export function dirsToExpand(input: { - level: number - filter?: { dirs: Set } - expanded: (dir: string) => boolean -}) { - if (input.level !== 0) return [] - if (!input.filter) return [] - return [...input.filter.dirs].filter((dir) => !input.expanded(dir)) -} - -const kindLabel = (kind: Kind) => { - if (kind === "add") return "A" - if (kind === "del") return "D" - return "M" -} - -const kindTextColor = (kind: Kind) => { - if (kind === "add") return "color: var(--icon-diff-add-base)" - if (kind === "del") return "color: var(--icon-diff-delete-base)" - return "color: var(--icon-diff-modified-base)" -} - -const kindDotColor = (kind: Kind) => { - if (kind === "add") return "background-color: var(--icon-diff-add-base)" - if (kind === "del") return "background-color: var(--icon-diff-delete-base)" - return "background-color: var(--icon-diff-modified-base)" -} - -const visibleKind = (node: FileNode, kinds?: ReadonlyMap, marks?: Set) => { - const kind = kinds?.get(node.path) - if (!kind) return - if (!marks?.has(node.path)) return - return kind -} - -const buildDragImage = (target: HTMLElement) => { - const icon = target.querySelector('[data-component="file-icon"]') ?? target.querySelector("svg") - const text = target.querySelector("span") - if (!icon || !text) return - - const image = document.createElement("div") - image.className = - "flex items-center gap-x-2 px-2 py-1 bg-surface-raised-base rounded-md border border-border-base text-12-regular text-text-strong" - image.style.position = "absolute" - image.style.top = "-1000px" - image.innerHTML = (icon as SVGElement).outerHTML + (text as HTMLSpanElement).outerHTML - return image -} - -const withFileDragImage = (event: DragEvent) => { - const image = buildDragImage(event.currentTarget as HTMLElement) - if (!image) return - document.body.appendChild(image) - event.dataTransfer?.setDragImage(image, 0, 12) - setTimeout(() => document.body.removeChild(image), 0) -} - -const FileTreeNode = ( - p: ParentProps & - ComponentProps<"div"> & - ComponentProps<"button"> & { - node: FileNode - level: number - active?: string - nodeClass?: string - draggable: boolean - kinds?: ReadonlyMap - marks?: Set - as?: "div" | "button" - }, -) => { - const [local, rest] = splitProps(p, [ - "node", - "level", - "active", - "nodeClass", - "draggable", - "kinds", - "marks", - "as", - "children", - "class", - "classList", - ]) - const kind = () => visibleKind(local.node, local.kinds, local.marks) - const active = () => !!kind() && !local.node.ignored - const color = () => { - const value = kind() - if (!value) return - return kindTextColor(value) - } - - return ( - { - if (!local.draggable) return - event.dataTransfer?.setData("text/plain", `file:${local.node.path}`) - event.dataTransfer?.setData("text/uri-list", pathToFileUrl(local.node.path)) - if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy" - withFileDragImage(event) - }} - {...rest} - > - {local.children} - - {local.node.name} - - {(() => { - const value = kind() - if (!value) return null - if (local.node.type === "file") { - return ( - - {kindLabel(value)} - - ) - } - return
- })()} - - ) -} - -export default function FileTree(props: { - path: string - class?: string - nodeClass?: string - active?: string - level?: number - allowed?: readonly string[] - modified?: readonly string[] - kinds?: ReadonlyMap - draggable?: boolean - onFileClick?: (file: FileNode) => void - - _filter?: Filter - _marks?: Set - _deeps?: Map - _kinds?: ReadonlyMap - _chain?: readonly string[] -}) { - const file = useFile() - const level = props.level ?? 0 - const draggable = () => props.draggable ?? true - - const key = (p: string) => - file - .normalize(p) - .replace(/[\\/]+$/, "") - .replaceAll("\\", "/") - const chain = props._chain ? [...props._chain, key(props.path)] : [key(props.path)] - - const filter = createMemo(() => { - if (props._filter) return props._filter - - const allowed = props.allowed - if (!allowed) return - - const files = new Set(allowed) - const dirs = new Set() - - for (const item of allowed) { - const parts = item.split("/") - const parents = parts.slice(0, -1) - for (const [idx] of parents.entries()) { - const dir = parents.slice(0, idx + 1).join("/") - if (dir) dirs.add(dir) - } - } - - return { files, dirs } - }) - - const marks = createMemo(() => { - if (props._marks) return props._marks - - const out = new Set() - for (const item of props.modified ?? []) out.add(item) - for (const item of props.kinds?.keys() ?? []) out.add(item) - if (out.size === 0) return - return out - }) - - const kinds = createMemo(() => { - if (props._kinds) return props._kinds - return props.kinds - }) - - const deeps = createMemo(() => { - if (props._deeps) return props._deeps - - const out = new Map() - - const root = props.path - if (!(file.tree.state(root)?.expanded ?? false)) return out - - const seen = new Set() - const stack: { dir: string; lvl: number; i: number; kids: string[]; max: number }[] = [] - - const push = (dir: string, lvl: number) => { - const id = key(dir) - if (seen.has(id)) return - seen.add(id) - - const kids = file.tree - .children(dir) - .filter((node) => node.type === "directory" && (file.tree.state(node.path)?.expanded ?? false)) - .map((node) => node.path) - - stack.push({ dir, lvl, i: 0, kids, max: lvl }) - } - - push(root, level - 1) - - while (stack.length > 0) { - const top = stack[stack.length - 1]! - - if (top.i < top.kids.length) { - const next = top.kids[top.i]! - top.i++ - push(next, top.lvl + 1) - continue - } - - out.set(top.dir, top.max) - stack.pop() - - const parent = stack[stack.length - 1] - if (!parent) continue - parent.max = Math.max(parent.max, top.max) - } - - return out - }) - - createEffect(() => { - const current = filter() - const dirs = dirsToExpand({ - level, - filter: current, - expanded: (dir) => untrack(() => file.tree.state(dir)?.expanded) ?? false, - }) - for (const dir of dirs) file.tree.expand(dir) - }) - - createEffect( - on( - () => props.path, - (path) => { - const dir = untrack(() => file.tree.state(path)) - if (!shouldListRoot({ level, dir })) return - void file.tree.list(path) - }, - { defer: false }, - ), - ) - - const nodes = createMemo(() => { - const nodes = file.tree.children(props.path) - const current = filter() - if (!current) return nodes - - const parent = (path: string) => { - const idx = path.lastIndexOf("/") - if (idx === -1) return "" - return path.slice(0, idx) - } - - const leaf = (path: string) => { - const idx = path.lastIndexOf("/") - return idx === -1 ? path : path.slice(idx + 1) - } - - const out = nodes.filter((node) => { - if (node.type === "file") return current.files.has(node.path) - return current.dirs.has(node.path) - }) - - const seen = new Set(out.map((node) => node.path)) - - for (const dir of current.dirs) { - if (parent(dir) !== props.path) continue - if (seen.has(dir)) continue - out.push({ - name: leaf(dir), - path: dir, - absolute: dir, - type: "directory", - ignored: false, - }) - seen.add(dir) - } - - for (const item of current.files) { - if (parent(item) !== props.path) continue - if (seen.has(item)) continue - out.push({ - name: leaf(item), - path: item, - absolute: item, - type: "file", - ignored: false, - }) - seen.add(item) - } - - out.sort((a, b) => { - if (a.type !== b.type) { - return a.type === "directory" ? -1 : 1 - } - return a.name.localeCompare(b.name) - }) - - return out - }) - - return ( -
- - {(node) => { - const expanded = () => file.tree.state(node.path)?.expanded ?? false - const deep = () => deeps().get(node.path) ?? -1 - const kind = () => visibleKind(node, kinds(), marks()) - const active = () => !!kind() && !node.ignored - - return ( - - - (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))} - > - - -
- -
-
-
- -
- ...
} - > - - -
-
-
- - props.onFileClick?.(node)} - > -
- - - - - - - - - - - - - - - - - - ) - }} - -
- ) -} diff --git a/packages/app/src/components/link.tsx b/packages/app/src/components/link.tsx deleted file mode 100644 index 85f7efc539e..00000000000 --- a/packages/app/src/components/link.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { ComponentProps, splitProps } from "solid-js" -import { usePlatform } from "@/context/platform" - -export interface LinkProps extends Omit, "href"> { - href: string -} - -export function Link(props: LinkProps) { - const platform = usePlatform() - const [local, rest] = splitProps(props, ["href", "children", "class"]) - - return ( - { - if (!local.href) return - event.preventDefault() - platform.openLink(local.href) - }} - {...rest} - > - {local.children} - - ) -} diff --git a/packages/app/src/components/model-tooltip.tsx b/packages/app/src/components/model-tooltip.tsx deleted file mode 100644 index 53164dae85e..00000000000 --- a/packages/app/src/components/model-tooltip.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { Show, type Component } from "solid-js" -import { useLanguage } from "@/context/language" - -type InputKey = "text" | "image" | "audio" | "video" | "pdf" -type InputMap = Record - -type ModelInfo = { - id: string - name: string - provider: { - name: string - } - capabilities?: { - reasoning: boolean - input: InputMap - } - modalities?: { - input: Array - } - reasoning?: boolean - limit: { - context: number - } -} - -export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean }> = (props) => { - const language = useLanguage() - const sourceName = (model: ModelInfo) => { - const value = `${model.id} ${model.name}`.toLowerCase() - - if (/claude|anthropic/.test(value)) return language.t("model.provider.anthropic") - if (/gpt|o[1-4]|codex|openai/.test(value)) return language.t("model.provider.openai") - if (/gemini|palm|bard|google/.test(value)) return language.t("model.provider.google") - if (/grok|xai/.test(value)) return language.t("model.provider.xai") - if (/llama|meta/.test(value)) return language.t("model.provider.meta") - - return model.provider.name - } - const inputLabel = (value: string) => { - if (value === "text") return language.t("model.input.text") - if (value === "image") return language.t("model.input.image") - if (value === "audio") return language.t("model.input.audio") - if (value === "video") return language.t("model.input.video") - if (value === "pdf") return language.t("model.input.pdf") - return value - } - const title = () => { - const tags: Array = [] - if (props.latest) tags.push(language.t("model.tag.latest")) - if (props.free) tags.push(language.t("model.tag.free")) - const suffix = tags.length ? ` (${tags.join(", ")})` : "" - return `${sourceName(props.model)} ${props.model.name}${suffix}` - } - const inputs = () => { - if (props.model.capabilities) { - const input = props.model.capabilities.input - const order: Array = ["text", "image", "audio", "video", "pdf"] - const entries = order.filter((key) => input[key]).map((key) => inputLabel(key)) - return entries.length ? entries.join(", ") : undefined - } - const raw = props.model.modalities?.input - if (!raw) return - const entries = raw.map((value) => inputLabel(value)) - return entries.length ? entries.join(", ") : undefined - } - const reasoning = () => { - if (props.model.capabilities) - return props.model.capabilities.reasoning - ? language.t("model.tooltip.reasoning.allowed") - : language.t("model.tooltip.reasoning.none") - return props.model.reasoning - ? language.t("model.tooltip.reasoning.allowed") - : language.t("model.tooltip.reasoning.none") - } - const context = () => language.t("model.tooltip.context", { limit: props.model.limit.context.toLocaleString() }) - - return ( -
-
{title()}
- - {(value) => ( -
- {language.t("model.tooltip.allows", { inputs: value() })} -
- )} -
-
{reasoning()}
-
{context()}
-
- ) -} diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx deleted file mode 100644 index 9d7e2394bfe..00000000000 --- a/packages/app/src/components/prompt-input.tsx +++ /dev/null @@ -1,1619 +0,0 @@ -import { useFilteredList } from "@opencode-ai/ui/hooks" -import { useSpring } from "@opencode-ai/ui/motion-spring" -import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal, createResource } from "solid-js" -import { createStore } from "solid-js/store" -import { useLocal } from "@/context/local" -import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file" -import { - ContentPart, - DEFAULT_PROMPT, - isPromptEqual, - Prompt, - usePrompt, - ImageAttachmentPart, - AgentPart, - FileAttachmentPart, -} from "@/context/prompt" -import { useLayout } from "@/context/layout" -import { useSDK } from "@/context/sdk" -import { useSync } from "@/context/sync" -import { useComments } from "@/context/comments" -import { Button } from "@opencode-ai/ui/button" -import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface" -import { Icon } from "@opencode-ai/ui/icon" -import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Select } from "@opencode-ai/ui/select" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { ModelSelectorPopover } from "@/components/dialog-select-model" -import { useProviders } from "@/hooks/use-providers" -import { useCommand } from "@/context/command" -import { Persist, persisted } from "@/utils/persist" -import { usePermission } from "@/context/permission" -import { useLanguage } from "@/context/language" -import { usePlatform } from "@/context/platform" -import { useSessionLayout } from "@/pages/session/session-layout" -import { createSessionTabs } from "@/pages/session/helpers" -import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom" -import { createPromptAttachments } from "./prompt-input/attachments" -import { ACCEPTED_FILE_TYPES } from "./prompt-input/files" -import { - canNavigateHistoryAtCursor, - navigatePromptHistory, - prependHistoryEntry, - type PromptHistoryComment, - type PromptHistoryEntry, - type PromptHistoryStoredEntry, - promptLength, -} from "./prompt-input/history" -import { createPromptSubmit, type FollowupDraft } from "./prompt-input/submit" -import { PromptPopover, type AtOption, type SlashCommand } from "./prompt-input/slash-popover" -import { PromptContextItems } from "./prompt-input/context-items" -import { PromptImageAttachments } from "./prompt-input/image-attachments" -import { PromptDragOverlay } from "./prompt-input/drag-overlay" -import { promptPlaceholder } from "./prompt-input/placeholder" -import { ImagePreview } from "@opencode-ai/ui/image-preview" -import { useQueries } from "@tanstack/solid-query" -import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap" - -interface PromptInputProps { - class?: string - ref?: (el: HTMLDivElement) => void - newSessionWorktree?: string - onNewSessionWorktreeReset?: () => void - edit?: { id: string; prompt: Prompt; context: FollowupDraft["context"] } - onEditLoaded?: () => void - shouldQueue?: () => boolean - onQueue?: (draft: FollowupDraft) => void - onAbort?: () => void - onSubmit?: () => void -} - -const EXAMPLES = [ - "prompt.example.1", - "prompt.example.2", - "prompt.example.3", - "prompt.example.4", - "prompt.example.5", - "prompt.example.6", - "prompt.example.7", - "prompt.example.8", - "prompt.example.9", - "prompt.example.10", - "prompt.example.11", - "prompt.example.12", - "prompt.example.13", - "prompt.example.14", - "prompt.example.15", - "prompt.example.16", - "prompt.example.17", - "prompt.example.18", - "prompt.example.19", - "prompt.example.20", - "prompt.example.21", - "prompt.example.22", - "prompt.example.23", - "prompt.example.24", - "prompt.example.25", -] as const - -const NON_EMPTY_TEXT = /[^\s\u200B]/ - -export const PromptInput: Component = (props) => { - const sdk = useSDK() - - const sync = useSync() - const local = useLocal() - const files = useFile() - const prompt = usePrompt() - const layout = useLayout() - const comments = useComments() - const dialog = useDialog() - const providers = useProviders() - const command = useCommand() - const permission = usePermission() - const language = useLanguage() - const platform = usePlatform() - const { params, tabs, view } = useSessionLayout() - let editorRef!: HTMLDivElement - let fileInputRef: HTMLInputElement | undefined - let scrollRef!: HTMLDivElement - let slashPopoverRef!: HTMLDivElement - - const mirror = { input: false } - const inset = 56 - const space = `${inset}px` - - const scrollCursorIntoView = () => { - const container = scrollRef - const selection = window.getSelection() - if (!container || !selection || selection.rangeCount === 0) return - - const range = selection.getRangeAt(0) - if (!editorRef.contains(range.startContainer)) return - - const cursor = getCursorPosition(editorRef) - const length = promptLength(prompt.current().filter((part) => part.type !== "image")) - if (cursor >= length) { - container.scrollTop = container.scrollHeight - return - } - - const rect = range.getClientRects().item(0) ?? range.getBoundingClientRect() - if (!rect.height) return - - const containerRect = container.getBoundingClientRect() - const top = rect.top - containerRect.top + container.scrollTop - const bottom = rect.bottom - containerRect.top + container.scrollTop - const padding = 12 - - if (top < container.scrollTop + padding) { - container.scrollTop = Math.max(0, top - padding) - return - } - - if (bottom > container.scrollTop + container.clientHeight - inset) { - container.scrollTop = bottom - container.clientHeight + inset - } - } - - const queueScroll = (count = 2) => { - requestAnimationFrame(() => { - scrollCursorIntoView() - if (count > 1) queueScroll(count - 1) - }) - } - - const activeFileTab = createSessionTabs({ - tabs, - pathFromTab: files.pathFromTab, - normalizeTab: (tab) => (tab.startsWith("file://") ? files.tab(tab) : tab), - }).activeFileTab - - const commentInReview = (path: string) => { - const sessionID = params.id - if (!sessionID) return false - - const diffs = sync.data.session_diff[sessionID] - if (!diffs) return false - return diffs.some((diff) => diff.file === path) - } - - const openComment = (item: { path: string; commentID?: string; commentOrigin?: "review" | "file" }) => { - if (!item.commentID) return - - const focus = { file: item.path, id: item.commentID } - comments.setActive(focus) - - const queueCommentFocus = (attempts = 6) => { - const schedule = (left: number) => { - requestAnimationFrame(() => { - comments.setFocus({ ...focus }) - if (left <= 0) return - requestAnimationFrame(() => { - const current = comments.focus() - if (!current) return - if (current.file !== focus.file || current.id !== focus.id) return - schedule(left - 1) - }) - }) - } - - schedule(attempts) - } - - const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path)) - if (wantsReview) { - if (!view().reviewPanel.opened()) view().reviewPanel.open() - layout.fileTree.setTab("changes") - tabs().setActive("review") - queueCommentFocus() - return - } - - if (!view().reviewPanel.opened()) view().reviewPanel.open() - layout.fileTree.setTab("all") - const tab = files.tab(item.path) - void tabs().open(tab) - tabs().setActive(tab) - void Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus()) - } - - const recent = createMemo(() => { - const all = tabs().all() - const active = activeFileTab() - const order = active ? [active, ...all.filter((x) => x !== active)] : all - const seen = new Set() - const paths: string[] = [] - - for (const tab of order) { - const path = files.pathFromTab(tab) - if (!path) continue - if (seen.has(path)) continue - seen.add(path) - paths.push(path) - } - - return paths - }) - const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) - const status = createMemo( - () => - sync.data.session_status[params.id ?? ""] ?? { - type: "idle", - }, - ) - const working = createMemo(() => status()?.type !== "idle") - const imageAttachments = createMemo(() => - prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"), - ) - - const [store, setStore] = createStore<{ - popover: "at" | "slash" | null - historyIndex: number - savedPrompt: PromptHistoryEntry | null - placeholder: number - draggingType: "image" | "@mention" | null - mode: "normal" | "shell" - applyingHistory: boolean - }>({ - popover: null, - historyIndex: -1, - savedPrompt: null as PromptHistoryEntry | null, - placeholder: Math.floor(Math.random() * EXAMPLES.length), - draggingType: null, - mode: "normal", - applyingHistory: false, - }) - - const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 }) - const motion = (value: number) => ({ - opacity: value, - transform: `scale(${0.98 + value * 0.02})`, - filter: `blur(${(1 - value) * 2}px)`, - "pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const), - }) - const buttons = createMemo(() => motion(buttonsSpring())) - const shell = createMemo(() => motion(1 - buttonsSpring())) - const control = createMemo(() => ({ height: "28px", ...buttons() })) - - const commentCount = createMemo(() => { - if (store.mode === "shell") return 0 - return prompt.context.items().filter((item) => !!item.comment?.trim()).length - }) - const blank = createMemo(() => { - const text = prompt - .current() - .map((part) => ("content" in part ? part.content : "")) - .join("") - return text.trim().length === 0 && imageAttachments().length === 0 && commentCount() === 0 - }) - const stopping = createMemo(() => working() && blank()) - const tip = () => { - if (stopping()) { - return ( -
- {language.t("prompt.action.stop")} - {language.t("common.key.esc")} -
- ) - } - - return ( -
- {language.t("prompt.action.send")} - -
- ) - } - - const contextItems = createMemo(() => { - const items = prompt.context.items() - if (store.mode !== "shell") return items - return items.filter((item) => !item.comment?.trim()) - }) - - const hasUserPrompt = createMemo(() => { - const sessionID = params.id - if (!sessionID) return false - const messages = sync.data.message[sessionID] - if (!messages) return false - return messages.some((m) => m.role === "user") - }) - - const [history, setHistory] = persisted( - Persist.global("prompt-history", ["prompt-history.v1"]), - createStore<{ - entries: PromptHistoryStoredEntry[] - }>({ - entries: [], - }), - ) - const [shellHistory, setShellHistory] = persisted( - Persist.global("prompt-history-shell", ["prompt-history-shell.v1"]), - createStore<{ - entries: PromptHistoryStoredEntry[] - }>({ - entries: [], - }), - ) - - const suggest = createMemo(() => !hasUserPrompt()) - - const placeholder = createMemo(() => - promptPlaceholder({ - mode: store.mode, - commentCount: commentCount(), - example: suggest() ? (store.mode === "shell" ? "git status" : language.t(EXAMPLES[store.placeholder])) : "", - suggest: suggest(), - t: (key, params) => language.t(key as Parameters[0], params as never), - }), - ) - - const historyComments = () => { - const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const)) - return prompt.context.items().flatMap((item) => { - if (item.type !== "file") return [] - const comment = item.comment?.trim() - if (!comment) return [] - - const selection = item.commentID ? byID.get(`${item.path}\n${item.commentID}`)?.selection : undefined - const nextSelection = - selection ?? - (item.selection - ? ({ - start: item.selection.startLine, - end: item.selection.endLine, - } satisfies SelectedLineRange) - : undefined) - if (!nextSelection) return [] - - return [ - { - id: item.commentID ?? item.key, - path: item.path, - selection: { ...nextSelection }, - comment, - time: item.commentID ? (byID.get(`${item.path}\n${item.commentID}`)?.time ?? Date.now()) : Date.now(), - origin: item.commentOrigin, - preview: item.preview, - } satisfies PromptHistoryComment, - ] - }) - } - - const applyHistoryComments = (items: PromptHistoryComment[]) => { - comments.replace( - items.map((item) => ({ - id: item.id, - file: item.path, - selection: { ...item.selection }, - comment: item.comment, - time: item.time, - })), - ) - prompt.context.replaceComments( - items.map((item) => ({ - type: "file" as const, - path: item.path, - selection: selectionFromLines(item.selection), - comment: item.comment, - commentID: item.id, - commentOrigin: item.origin, - preview: item.preview, - })), - ) - } - - const applyHistoryPrompt = (entry: PromptHistoryEntry, position: "start" | "end") => { - const p = entry.prompt - const length = position === "start" ? 0 : promptLength(p) - setStore("applyingHistory", true) - applyHistoryComments(entry.comments) - prompt.set(p, length) - requestAnimationFrame(() => { - editorRef.focus() - setCursorPosition(editorRef, length) - setStore("applyingHistory", false) - queueScroll() - }) - } - - const getCaretState = () => { - const selection = window.getSelection() - const textLength = promptLength(prompt.current()) - if (!selection || selection.rangeCount === 0) { - return { collapsed: false, cursorPosition: 0, textLength } - } - const anchorNode = selection.anchorNode - if (!anchorNode || !editorRef.contains(anchorNode)) { - return { collapsed: false, cursorPosition: 0, textLength } - } - return { - collapsed: selection.isCollapsed, - cursorPosition: getCursorPosition(editorRef), - textLength, - } - } - - const escBlur = () => platform.platform === "desktop" && platform.os === "macos" - - const pick = () => fileInputRef?.click() - - const setMode = (mode: "normal" | "shell") => { - setStore("mode", mode) - setStore("popover", null) - requestAnimationFrame(() => editorRef?.focus()) - } - - const shellModeKey = "mod+shift+x" - const normalModeKey = "mod+shift+e" - - command.register("prompt-input", () => [ - { - id: "file.attach", - title: language.t("prompt.action.attachFile"), - category: language.t("command.category.file"), - keybind: "mod+u", - disabled: store.mode !== "normal", - onSelect: pick, - }, - { - id: "prompt.mode.shell", - title: language.t("command.prompt.mode.shell"), - category: language.t("command.category.session"), - keybind: shellModeKey, - disabled: store.mode === "shell", - onSelect: () => setMode("shell"), - }, - { - id: "prompt.mode.normal", - title: language.t("command.prompt.mode.normal"), - category: language.t("command.category.session"), - keybind: normalModeKey, - disabled: store.mode === "normal", - onSelect: () => setMode("normal"), - }, - ]) - - const closePopover = () => setStore("popover", null) - - const resetHistoryNavigation = (force = false) => { - if (!force && (store.historyIndex < 0 || store.applyingHistory)) return - setStore("historyIndex", -1) - setStore("savedPrompt", null) - } - - const clearEditor = () => { - editorRef.innerHTML = "" - } - - const setEditorText = (text: string) => { - clearEditor() - editorRef.textContent = text - } - - const focusEditorEnd = () => { - requestAnimationFrame(() => { - editorRef.focus() - const range = document.createRange() - const selection = window.getSelection() - range.selectNodeContents(editorRef) - range.collapse(false) - selection?.removeAllRanges() - selection?.addRange(range) - }) - } - - const currentCursor = () => { - const selection = window.getSelection() - if (!selection || selection.rangeCount === 0 || !editorRef.contains(selection.anchorNode)) return null - return getCursorPosition(editorRef) - } - - const restoreFocus = () => { - requestAnimationFrame(() => { - const cursor = prompt.cursor() ?? promptLength(prompt.current()) - editorRef.focus() - setCursorPosition(editorRef, cursor) - queueScroll() - }) - } - - const renderEditorWithCursor = (parts: Prompt) => { - const cursor = currentCursor() - renderEditor(parts) - if (cursor !== null) setCursorPosition(editorRef, cursor) - } - - createEffect(() => { - params.id - if (params.id) return - if (!suggest()) return - const interval = setInterval(() => { - setStore("placeholder", (prev) => (prev + 1) % EXAMPLES.length) - }, 6500) - onCleanup(() => clearInterval(interval)) - }) - - const [composing, setComposing] = createSignal(false) - const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229 - - const handleBlur = () => { - closePopover() - setComposing(false) - } - - const handleCompositionStart = () => { - setComposing(true) - } - - const handleCompositionEnd = () => { - setComposing(false) - requestAnimationFrame(() => { - if (composing()) return - reconcile(prompt.current().filter((part) => part.type !== "image")) - }) - } - - const agentList = createMemo(() => - sync.data.agent - .filter((agent) => !agent.hidden && agent.mode !== "primary") - .map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })), - ) - const agentNames = createMemo(() => local.agent.list().map((agent) => agent.name)) - - const handleAtSelect = (option: AtOption | undefined) => { - if (!option) return - if (option.type === "agent") { - addPart({ type: "agent", name: option.name, content: "@" + option.name, start: 0, end: 0 }) - } else { - addPart({ type: "file", path: option.path, content: "@" + option.path, start: 0, end: 0 }) - } - } - - const atKey = (x: AtOption | undefined) => { - if (!x) return "" - return x.type === "agent" ? `agent:${x.name}` : `file:${x.path}` - } - - const { - flat: atFlat, - active: atActive, - setActive: setAtActive, - onInput: atOnInput, - onKeyDown: atOnKeyDown, - } = useFilteredList({ - items: async (query) => { - const agents = agentList() - const open = recent() - const seen = new Set(open) - const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true })) - if (!query.trim()) return [...agents, ...pinned] - const paths = await files.searchFilesAndDirectories(query) - const fileOptions: AtOption[] = paths - .filter((path) => !seen.has(path)) - .map((path) => ({ type: "file", path, display: path })) - return [...agents, ...pinned, ...fileOptions] - }, - key: atKey, - filterKeys: ["display"], - groupBy: (item) => { - if (item.type === "agent") return "agent" - if (item.recent) return "recent" - return "file" - }, - sortGroupsBy: (a, b) => { - const rank = (category: string) => { - if (category === "agent") return 0 - if (category === "recent") return 1 - return 2 - } - return rank(a.category) - rank(b.category) - }, - onSelect: handleAtSelect, - }) - - const slashCommands = createMemo(() => { - const builtin = command.options - .filter((opt) => !opt.disabled && !opt.id.startsWith("suggested.") && opt.slash) - .map((opt) => ({ - id: opt.id, - trigger: opt.slash!, - title: opt.title, - description: opt.description, - keybind: opt.keybind, - type: "builtin" as const, - })) - - const custom = sync.data.command.map((cmd) => ({ - id: `custom.${cmd.name}`, - trigger: cmd.name, - title: cmd.name, - description: cmd.description, - type: "custom" as const, - source: cmd.source, - })) - - return [...custom, ...builtin] - }) - - const handleSlashSelect = (cmd: SlashCommand | undefined) => { - if (!cmd) return - closePopover() - const images = imageAttachments() - - if (cmd.type === "custom") { - const text = `/${cmd.trigger} ` - setEditorText(text) - prompt.set([{ type: "text", content: text, start: 0, end: text.length }, ...images], text.length) - focusEditorEnd() - return - } - - clearEditor() - prompt.set([...DEFAULT_PROMPT, ...images], 0) - command.trigger(cmd.id, "slash") - } - - const { - flat: slashFlat, - active: slashActive, - setActive: setSlashActive, - onInput: slashOnInput, - onKeyDown: slashOnKeyDown, - } = useFilteredList({ - items: slashCommands, - key: (x) => x?.id, - filterKeys: ["trigger", "title"], - onSelect: handleSlashSelect, - }) - - const createPill = (part: FileAttachmentPart | AgentPart) => { - const pill = document.createElement("span") - pill.textContent = part.content - pill.setAttribute("data-type", part.type) - if (part.type === "file") pill.setAttribute("data-path", part.path) - if (part.type === "agent") pill.setAttribute("data-name", part.name) - pill.setAttribute("contenteditable", "false") - pill.style.userSelect = "text" - pill.style.cursor = "default" - return pill - } - - const isNormalizedEditor = () => - Array.from(editorRef.childNodes).every((node) => { - if (node.nodeType === Node.TEXT_NODE) { - const text = node.textContent ?? "" - if (!text.includes("\u200B")) return true - if (text !== "\u200B") return false - - const prev = node.previousSibling - const next = node.nextSibling - const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR" - return !!prevIsBr && !next - } - if (node.nodeType !== Node.ELEMENT_NODE) return false - const el = node as HTMLElement - if (el.dataset.type === "file") return true - if (el.dataset.type === "agent") return true - return el.tagName === "BR" - }) - - const renderEditor = (parts: Prompt) => { - clearEditor() - for (const part of parts) { - if (part.type === "text") { - editorRef.appendChild(createTextFragment(part.content)) - continue - } - if (part.type === "file" || part.type === "agent") { - editorRef.appendChild(createPill(part)) - } - } - - const last = editorRef.lastChild - if (last?.nodeType === Node.ELEMENT_NODE && (last as HTMLElement).tagName === "BR") { - editorRef.appendChild(document.createTextNode("\u200B")) - } - } - - // Auto-scroll active command into view when navigating with keyboard - createEffect(() => { - const activeId = slashActive() - if (!activeId || !slashPopoverRef) return - - requestAnimationFrame(() => { - const element = slashPopoverRef.querySelector(`[data-slash-id="${activeId}"]`) - element?.scrollIntoView({ block: "nearest", behavior: "smooth" }) - }) - }) - const selectPopoverActive = () => { - if (store.popover === "at") { - const items = atFlat() - if (items.length === 0) return - const active = atActive() - const item = items.find((entry) => atKey(entry) === active) ?? items[0] - handleAtSelect(item) - return - } - - if (store.popover === "slash") { - const items = slashFlat() - if (items.length === 0) return - const active = slashActive() - const item = items.find((entry) => entry.id === active) ?? items[0] - handleSlashSelect(item) - } - } - - const reconcile = (input: Prompt) => { - if (mirror.input) { - mirror.input = false - if (isNormalizedEditor()) return - - renderEditorWithCursor(input) - return - } - - const dom = parseFromDOM() - if (isNormalizedEditor() && isPromptEqual(input, dom)) return - - renderEditorWithCursor(input) - } - - createEffect( - on( - () => prompt.current(), - (parts) => { - if (composing()) return - reconcile(parts.filter((part) => part.type !== "image")) - }, - ), - ) - - const parseFromDOM = (): Prompt => { - const parts: Prompt = [] - let position = 0 - let buffer = "" - - const flushText = () => { - let content = buffer - if (content.includes("\r")) content = content.replace(/\r\n?/g, "\n") - if (content.includes("\u200B")) content = content.replace(/\u200B/g, "") - buffer = "" - if (!content) return - parts.push({ type: "text", content, start: position, end: position + content.length }) - position += content.length - } - - const pushFile = (file: HTMLElement) => { - const content = file.textContent ?? "" - parts.push({ - type: "file", - path: file.dataset.path!, - content, - start: position, - end: position + content.length, - }) - position += content.length - } - - const pushAgent = (agent: HTMLElement) => { - const content = agent.textContent ?? "" - parts.push({ - type: "agent", - name: agent.dataset.name!, - content, - start: position, - end: position + content.length, - }) - position += content.length - } - - const visit = (node: Node) => { - if (node.nodeType === Node.TEXT_NODE) { - buffer += node.textContent ?? "" - return - } - if (node.nodeType !== Node.ELEMENT_NODE) return - - const el = node as HTMLElement - if (el.dataset.type === "file") { - flushText() - pushFile(el) - return - } - if (el.dataset.type === "agent") { - flushText() - pushAgent(el) - return - } - if (el.tagName === "BR") { - buffer += "\n" - return - } - - for (const child of Array.from(el.childNodes)) { - visit(child) - } - } - - const children = Array.from(editorRef.childNodes) - children.forEach((child, index) => { - const isBlock = child.nodeType === Node.ELEMENT_NODE && ["DIV", "P"].includes((child as HTMLElement).tagName) - visit(child) - if (isBlock && index < children.length - 1) { - buffer += "\n" - } - }) - - flushText() - - if (parts.length === 0) parts.push(...DEFAULT_PROMPT) - return parts - } - - const handleInput = () => { - const rawParts = parseFromDOM() - const images = imageAttachments() - const cursorPosition = getCursorPosition(editorRef) - const rawText = - rawParts.length === 1 && rawParts[0]?.type === "text" - ? rawParts[0].content - : rawParts.map((p) => ("content" in p ? p.content : "")).join("") - const hasNonText = rawParts.some((part) => part.type !== "text") - const shouldReset = !NON_EMPTY_TEXT.test(rawText) && !hasNonText && images.length === 0 - - if (shouldReset) { - closePopover() - resetHistoryNavigation() - if (prompt.dirty()) { - mirror.input = true - prompt.set(DEFAULT_PROMPT, 0) - } - queueScroll() - return - } - - const shellMode = store.mode === "shell" - - if (!shellMode) { - const atMatch = rawText.substring(0, cursorPosition).match(/@(\S*)$/) - const slashMatch = rawText.match(/^\/(\S*)$/) - - if (atMatch) { - atOnInput(atMatch[1]) - setStore("popover", "at") - } else if (slashMatch) { - slashOnInput(slashMatch[1]) - setStore("popover", "slash") - } else { - closePopover() - } - } else { - closePopover() - } - - resetHistoryNavigation() - - mirror.input = true - prompt.set([...rawParts, ...images], cursorPosition) - queueScroll() - } - - const addPart = (part: ContentPart) => { - if (part.type === "image") return false - - const selection = window.getSelection() - if (!selection) return false - - if (selection.rangeCount === 0 || !editorRef.contains(selection.anchorNode)) { - editorRef.focus() - const cursor = prompt.cursor() ?? promptLength(prompt.current()) - setCursorPosition(editorRef, cursor) - } - - if (selection.rangeCount === 0) return false - const range = selection.getRangeAt(0) - if (!editorRef.contains(range.startContainer)) return false - - if (part.type === "file" || part.type === "agent") { - const cursorPosition = getCursorPosition(editorRef) - const rawText = prompt - .current() - .map((p) => ("content" in p ? p.content : "")) - .join("") - const textBeforeCursor = rawText.substring(0, cursorPosition) - const atMatch = textBeforeCursor.match(/@(\S*)$/) - const pill = createPill(part) - const gap = document.createTextNode(" ") - - if (atMatch) { - const start = atMatch.index ?? cursorPosition - atMatch[0].length - setRangeEdge(editorRef, range, "start", start) - setRangeEdge(editorRef, range, "end", cursorPosition) - } - - range.deleteContents() - range.insertNode(gap) - range.insertNode(pill) - range.setStartAfter(gap) - range.collapse(true) - selection.removeAllRanges() - selection.addRange(range) - } - - if (part.type === "text") { - const fragment = createTextFragment(part.content) - const last = fragment.lastChild - range.deleteContents() - range.insertNode(fragment) - if (last) { - if (last.nodeType === Node.TEXT_NODE) { - const text = last.textContent ?? "" - if (text === "\u200B") { - range.setStart(last, 0) - } - if (text !== "\u200B") { - range.setStart(last, text.length) - } - } - if (last.nodeType !== Node.TEXT_NODE) { - const isBreak = last.nodeType === Node.ELEMENT_NODE && (last as HTMLElement).tagName === "BR" - const next = last.nextSibling - const emptyText = next?.nodeType === Node.TEXT_NODE && (next.textContent ?? "") === "" - if (isBreak && (!next || emptyText)) { - const placeholder = next && emptyText ? next : document.createTextNode("\u200B") - if (!next) last.parentNode?.insertBefore(placeholder, null) - placeholder.textContent = "\u200B" - range.setStart(placeholder, 0) - } else { - range.setStartAfter(last) - } - } - } - range.collapse(true) - selection.removeAllRanges() - selection.addRange(range) - } - - handleInput() - closePopover() - return true - } - - const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => { - const currentHistory = mode === "shell" ? shellHistory : history - const setCurrentHistory = mode === "shell" ? setShellHistory : setHistory - const next = prependHistoryEntry(currentHistory.entries, prompt, mode === "shell" ? [] : historyComments()) - if (next === currentHistory.entries) return - setCurrentHistory("entries", next) - } - - createEffect( - on( - () => props.edit?.id, - (id) => { - const edit = props.edit - if (!id || !edit) return - - for (const item of prompt.context.items()) { - prompt.context.remove(item.key) - } - - for (const item of edit.context) { - prompt.context.add({ - type: item.type, - path: item.path, - selection: item.selection, - comment: item.comment, - commentID: item.commentID, - commentOrigin: item.commentOrigin, - preview: item.preview, - }) - } - - setStore("mode", "normal") - setStore("popover", null) - setStore("historyIndex", -1) - setStore("savedPrompt", null) - prompt.set(edit.prompt, promptLength(edit.prompt)) - requestAnimationFrame(() => { - editorRef.focus() - setCursorPosition(editorRef, promptLength(edit.prompt)) - queueScroll() - }) - props.onEditLoaded?.() - }, - { defer: true }, - ), - ) - - const navigateHistory = (direction: "up" | "down") => { - const result = navigatePromptHistory({ - direction, - entries: store.mode === "shell" ? shellHistory.entries : history.entries, - historyIndex: store.historyIndex, - currentPrompt: prompt.current(), - currentComments: historyComments(), - savedPrompt: store.savedPrompt, - }) - if (!result.handled) return false - setStore("historyIndex", result.historyIndex) - setStore("savedPrompt", result.savedPrompt) - applyHistoryPrompt(result.entry, result.cursor) - return true - } - - const { addAttachments, removeAttachment, handlePaste } = createPromptAttachments({ - editor: () => editorRef, - isDialogActive: () => !!dialog.active, - setDraggingType: (type) => setStore("draggingType", type), - focusEditor: () => { - editorRef.focus() - setCursorPosition(editorRef, promptLength(prompt.current())) - }, - addPart, - readClipboardImage: platform.readClipboardImage, - }) - - const variants = createMemo(() => ["default", ...local.model.variant.list()]) - const accepting = createMemo(() => { - const id = params.id - if (!id) return permission.isAutoAcceptingDirectory(sdk.directory) - return permission.isAutoAccepting(id, sdk.directory) - }) - - const { abort, handleSubmit } = createPromptSubmit({ - info, - imageAttachments, - commentCount, - autoAccept: () => accepting(), - mode: () => store.mode, - working, - editor: () => editorRef, - queueScroll, - promptLength, - addToHistory, - resetHistoryNavigation: () => { - resetHistoryNavigation(true) - }, - setMode: (mode) => setStore("mode", mode), - setPopover: (popover) => setStore("popover", popover), - newSessionWorktree: () => props.newSessionWorktree, - onNewSessionWorktreeReset: props.onNewSessionWorktreeReset, - shouldQueue: props.shouldQueue, - onQueue: props.onQueue, - onAbort: props.onAbort, - onSubmit: props.onSubmit, - }) - - const handleKeyDown = (event: KeyboardEvent) => { - if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "u") { - event.preventDefault() - if (store.mode !== "normal") return - pick() - return - } - - if (event.key === "Backspace") { - const selection = window.getSelection() - if (selection && selection.isCollapsed) { - const node = selection.anchorNode - const offset = selection.anchorOffset - if (node && node.nodeType === Node.TEXT_NODE) { - const text = node.textContent ?? "" - if (/^\u200B+$/.test(text) && offset > 0) { - const range = document.createRange() - range.setStart(node, 0) - range.collapse(true) - selection.removeAllRanges() - selection.addRange(range) - } - } - } - } - - if (event.key === "!" && store.mode === "normal") { - const cursorPosition = getCursorPosition(editorRef) - if (cursorPosition === 0) { - setStore("mode", "shell") - setStore("popover", null) - event.preventDefault() - return - } - } - - if (event.key === "Escape") { - if (store.popover) { - closePopover() - event.preventDefault() - event.stopPropagation() - return - } - - if (store.mode === "shell") { - setStore("mode", "normal") - event.preventDefault() - event.stopPropagation() - return - } - - if (working()) { - void abort() - event.preventDefault() - event.stopPropagation() - return - } - - if (escBlur()) { - editorRef.blur() - event.preventDefault() - event.stopPropagation() - return - } - } - - if (store.mode === "shell") { - const { collapsed, cursorPosition, textLength } = getCaretState() - if (event.key === "Backspace" && collapsed && cursorPosition === 0 && textLength === 0) { - setStore("mode", "normal") - event.preventDefault() - return - } - } - - // Handle Shift+Enter BEFORE IME check - Shift+Enter is never used for IME input - // and should always insert a newline regardless of composition state - if (event.key === "Enter" && event.shiftKey) { - addPart({ type: "text", content: "\n", start: 0, end: 0 }) - event.preventDefault() - return - } - - if (event.key === "Enter" && isImeComposing(event)) { - return - } - - const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey - - if (store.popover) { - if (event.key === "Tab") { - selectPopoverActive() - event.preventDefault() - return - } - const nav = event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter" - const ctrlNav = ctrl && (event.key === "n" || event.key === "p") - if (nav || ctrlNav) { - if (store.popover === "at") { - atOnKeyDown(event) - event.preventDefault() - return - } - if (store.popover === "slash") { - slashOnKeyDown(event) - } - event.preventDefault() - return - } - } - - if (ctrl && event.code === "KeyG") { - if (store.popover) { - closePopover() - event.preventDefault() - return - } - if (working()) { - void abort() - event.preventDefault() - } - return - } - - if (event.key === "ArrowUp" || event.key === "ArrowDown") { - if (event.altKey || event.ctrlKey || event.metaKey) return - const { collapsed } = getCaretState() - if (!collapsed) return - - const cursorPosition = getCursorPosition(editorRef) - const textContent = prompt - .current() - .map((part) => ("content" in part ? part.content : "")) - .join("") - const direction = event.key === "ArrowUp" ? "up" : "down" - if (!canNavigateHistoryAtCursor(direction, textContent, cursorPosition, store.historyIndex >= 0)) return - if (navigateHistory(direction)) { - event.preventDefault() - } - return - } - - // Note: Shift+Enter is handled earlier, before IME check - if (event.key === "Enter" && !event.shiftKey) { - event.preventDefault() - if (event.repeat) return - if ( - working() && - prompt - .current() - .map((part) => ("content" in part ? part.content : "")) - .join("") - .trim().length === 0 && - imageAttachments().length === 0 && - commentCount() === 0 - ) { - return - } - void handleSubmit(event) - } - } - - const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({ - queries: [loadAgentsQuery(sdk.directory), loadProvidersQuery(null), loadProvidersQuery(sdk.directory)], - })) - - const agentsLoading = () => agentsQuery.isLoading - const agentsShouldFadeIn = createMemo((prev) => prev ?? agentsLoading()) - const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading - const providersShouldFadeIn = createMemo((prev) => prev ?? providersLoading()) - - const [promptReady] = createResource( - () => prompt.ready().promise, - (p) => p, - ) - - return ( -
- {(promptReady(), null)} - (slashPopoverRef = el)} - atFlat={atFlat()} - atActive={atActive() ?? undefined} - atKey={atKey} - setAtActive={setAtActive} - onAtSelect={handleAtSelect} - slashFlat={slashFlat()} - slashActive={slashActive() ?? undefined} - setSlashActive={setSlashActive} - onSlashSelect={handleSlashSelect} - commandKeybind={command.keybind} - t={(key) => language.t(key as Parameters[0])} - /> - - - { - const active = comments.active() - return !!item.commentID && item.commentID === active?.id && item.path === active?.file - }} - openComment={openComment} - remove={(item) => { - if (item.commentID) comments.remove(item.path, item.commentID) - prompt.context.remove(item.key) - }} - t={(key) => language.t(key as Parameters[0])} - /> - - dialog.show(() => ) - } - onRemove={removeAttachment} - removeLabel={language.t("prompt.attachment.remove")} - /> -
{ - const target = e.target - if (!(target instanceof HTMLElement)) return - if (target.closest('[data-action="prompt-attach"], [data-action="prompt-submit"]')) { - return - } - editorRef?.focus() - }} - > -
(scrollRef = el)} - style={{ "scroll-padding-bottom": space }} - > -
{ - editorRef = el - props.ref?.(el) - }} - role="textbox" - aria-multiline="true" - aria-label={placeholder()} - contenteditable="true" - autocapitalize={store.mode === "normal" ? "sentences" : "off"} - autocorrect={store.mode === "normal" ? "on" : "off"} - spellcheck={store.mode === "normal"} - inputMode="text" - // @ts-expect-error - autocomplete="off" - onInput={handleInput} - onPaste={handlePaste} - onCompositionStart={handleCompositionStart} - onCompositionEnd={handleCompositionEnd} - onBlur={handleBlur} - onKeyDown={handleKeyDown} - classList={{ - "select-text": true, - "w-full pl-3 pr-2 pt-2 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, - "[&_[data-type=file]]:text-syntax-property": true, - "[&_[data-type=agent]]:text-syntax-type": true, - "font-mono!": store.mode === "shell", - }} - style={{ "padding-bottom": space }} - /> -
- {placeholder()} -
-
- - - - - -
-
-
- - {language.t("prompt.mode.shell")} -
- -
-
- -
- - (x === "default" ? language.t("common.default") : x)} - onSelect={(value) => { - local.model.variant.set(value === "default" ? undefined : value) - restoreFocus() - }} - class="capitalize max-w-[160px] text-text-base" - valueClass="truncate text-13-regular text-text-base" - triggerStyle={control()} - triggerProps={{ "data-action": "prompt-model-variant" }} - variant="ghost" - /> - -
-
- - -
-
-
- - -
- ) -} diff --git a/packages/app/src/components/prompt-input/attachments.test.ts b/packages/app/src/components/prompt-input/attachments.test.ts deleted file mode 100644 index 43f7d425bd1..00000000000 --- a/packages/app/src/components/prompt-input/attachments.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { attachmentMime } from "./files" -import { pasteMode } from "./paste" - -describe("attachmentMime", () => { - test("keeps PDFs when the browser reports the mime", async () => { - const file = new File(["%PDF-1.7"], "guide.pdf", { type: "application/pdf" }) - expect(await attachmentMime(file)).toBe("application/pdf") - }) - - test("normalizes structured text types to text/plain", async () => { - const file = new File(['{"ok":true}\n'], "data.json", { type: "application/json" }) - expect(await attachmentMime(file)).toBe("text/plain") - }) - - test("accepts text files even with a misleading browser mime", async () => { - const file = new File(["export const x = 1\n"], "main.ts", { type: "video/mp2t" }) - expect(await attachmentMime(file)).toBe("text/plain") - }) - - test("rejects binary files", async () => { - const file = new File([Uint8Array.of(0, 255, 1, 2)], "blob.bin", { type: "application/octet-stream" }) - expect(await attachmentMime(file)).toBeUndefined() - }) -}) - -describe("pasteMode", () => { - test("uses native paste for short single-line text", () => { - expect(pasteMode("hello world")).toBe("native") - }) - - test("uses manual paste for multiline text", () => { - expect( - pasteMode(`{ - "ok": true -}`), - ).toBe("manual") - expect(pasteMode("a\r\nb")).toBe("manual") - }) - - test("uses manual paste for large text", () => { - expect(pasteMode("x".repeat(8000))).toBe("manual") - }) -}) diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts deleted file mode 100644 index f12a4210c08..00000000000 --- a/packages/app/src/components/prompt-input/attachments.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { onMount } from "solid-js" -import { makeEventListener } from "@solid-primitives/event-listener" -import { showToast } from "@opencode-ai/ui/toast" -import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt" -import { useLanguage } from "@/context/language" -import { uuid } from "@/utils/uuid" -import { getCursorPosition } from "./editor-dom" -import { attachmentMime } from "./files" -import { normalizePaste, pasteMode } from "./paste" - -function dataUrl(file: File, mime: string) { - return new Promise((resolve) => { - const reader = new FileReader() - reader.addEventListener("error", () => resolve("")) - reader.addEventListener("load", () => { - const value = typeof reader.result === "string" ? reader.result : "" - const idx = value.indexOf(",") - if (idx === -1) { - resolve(value) - return - } - resolve(`data:${mime};base64,${value.slice(idx + 1)}`) - }) - reader.readAsDataURL(file) - }) -} - -type PromptAttachmentsInput = { - editor: () => HTMLDivElement | undefined - isDialogActive: () => boolean - setDraggingType: (type: "image" | "@mention" | null) => void - focusEditor: () => void - addPart: (part: ContentPart) => boolean - readClipboardImage?: () => Promise -} - -export function createPromptAttachments(input: PromptAttachmentsInput) { - const prompt = usePrompt() - const language = useLanguage() - - const warn = () => { - showToast({ - title: language.t("prompt.toast.pasteUnsupported.title"), - description: language.t("prompt.toast.pasteUnsupported.description"), - }) - } - - const add = async (file: File, toast = true) => { - const mime = await attachmentMime(file) - if (!mime) { - if (toast) warn() - return false - } - - const editor = input.editor() - if (!editor) return false - - const url = await dataUrl(file, mime) - if (!url) return false - - const attachment: ImageAttachmentPart = { - type: "image", - id: uuid(), - filename: file.name, - mime, - dataUrl: url, - } - const cursor = prompt.cursor() ?? getCursorPosition(editor) - prompt.set([...prompt.current(), attachment], cursor) - return true - } - - const addAttachment = (file: File) => add(file) - - const addAttachments = async (files: File[], toast = true) => { - let found = false - - for (const file of files) { - const ok = await add(file, false) - if (ok) found = true - } - - if (!found && files.length > 0 && toast) warn() - return found - } - - const removeAttachment = (id: string) => { - const current = prompt.current() - const next = current.filter((part) => part.type !== "image" || part.id !== id) - prompt.set(next, prompt.cursor()) - } - - const handlePaste = async (event: ClipboardEvent) => { - const clipboardData = event.clipboardData - if (!clipboardData) return - - event.preventDefault() - event.stopPropagation() - - const files = Array.from(clipboardData.items).flatMap((item) => { - if (item.kind !== "file") return [] - const file = item.getAsFile() - return file ? [file] : [] - }) - - if (files.length > 0) { - await addAttachments(files) - return - } - - const plainText = clipboardData.getData("text/plain") ?? "" - - // Desktop: Browser clipboard has no images and no text, try platform's native clipboard for images - if (input.readClipboardImage && !plainText) { - const file = await input.readClipboardImage() - if (file) { - await addAttachment(file) - return - } - } - - if (!plainText) return - - const text = normalizePaste(plainText) - - const put = () => { - if (input.addPart({ type: "text", content: text, start: 0, end: 0 })) return true - input.focusEditor() - return input.addPart({ type: "text", content: text, start: 0, end: 0 }) - } - - if (pasteMode(text) === "manual") { - put() - return - } - - const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, text) - if (inserted) return - - put() - } - - const handleGlobalDragOver = (event: DragEvent) => { - if (input.isDialogActive()) return - - event.preventDefault() - const hasFiles = event.dataTransfer?.types.includes("Files") - const hasText = event.dataTransfer?.types.includes("text/plain") - if (hasFiles) { - input.setDraggingType("image") - } else if (hasText) { - input.setDraggingType("@mention") - } - } - - const handleGlobalDragLeave = (event: DragEvent) => { - if (input.isDialogActive()) return - if (!event.relatedTarget) { - input.setDraggingType(null) - } - } - - const handleGlobalDrop = async (event: DragEvent) => { - if (input.isDialogActive()) return - - event.preventDefault() - input.setDraggingType(null) - - const plainText = event.dataTransfer?.getData("text/plain") - const filePrefix = "file:" - if (plainText?.startsWith(filePrefix)) { - const filePath = plainText.slice(filePrefix.length) - input.focusEditor() - input.addPart({ type: "file", path: filePath, content: "@" + filePath, start: 0, end: 0 }) - return - } - - const dropped = event.dataTransfer?.files - if (!dropped) return - - await addAttachments(Array.from(dropped)) - } - - onMount(() => { - makeEventListener(document, "dragover", handleGlobalDragOver) - makeEventListener(document, "dragleave", handleGlobalDragLeave) - makeEventListener(document, "drop", handleGlobalDrop) - }) - - return { - addAttachment, - addAttachments, - removeAttachment, - handlePaste, - } -} diff --git a/packages/app/src/components/prompt-input/build-request-parts.test.ts b/packages/app/src/components/prompt-input/build-request-parts.test.ts deleted file mode 100644 index 06c37733100..00000000000 --- a/packages/app/src/components/prompt-input/build-request-parts.test.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { Prompt } from "@/context/prompt" -import { buildRequestParts } from "./build-request-parts" - -describe("buildRequestParts", () => { - test("builds typed request and optimistic parts without cast path", () => { - const prompt: Prompt = [ - { type: "text", content: "hello", start: 0, end: 5 }, - { - type: "file", - path: "src/foo.ts", - content: "@src/foo.ts", - start: 5, - end: 16, - selection: { startLine: 4, startChar: 1, endLine: 6, endChar: 1 }, - }, - { type: "agent", name: "planner", content: "@planner", start: 16, end: 24 }, - ] - - const result = buildRequestParts({ - prompt, - context: [{ key: "ctx:1", type: "file", path: "src/bar.ts", comment: "check this" }], - images: [ - { type: "image", id: "img_1", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" }, - ], - text: "hello @src/foo.ts @planner", - messageID: "msg_1", - sessionID: "ses_1", - sessionDirectory: "/repo", - }) - - expect(result.requestParts[0]?.type).toBe("text") - expect(result.requestParts.some((part) => part.type === "agent")).toBe(true) - expect( - result.requestParts.some((part) => part.type === "file" && part.url.startsWith("file:///repo/src/foo.ts")), - ).toBe(true) - expect(result.requestParts.some((part) => part.type === "text" && part.synthetic)).toBe(true) - expect( - result.requestParts.some( - (part) => - part.type === "text" && - part.synthetic && - part.metadata?.opencodeComment && - (part.metadata.opencodeComment as { comment?: string }).comment === "check this", - ), - ).toBe(true) - - expect(result.optimisticParts).toHaveLength(result.requestParts.length) - expect(result.optimisticParts.every((part) => part.sessionID === "ses_1" && part.messageID === "msg_1")).toBe(true) - }) - - test("keeps multiple uploaded attachments in order", () => { - const result = buildRequestParts({ - prompt: [{ type: "text", content: "check these", start: 0, end: 11 }], - context: [], - images: [ - { type: "image", id: "img_1", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" }, - { - type: "image", - id: "img_2", - filename: "b.pdf", - mime: "application/pdf", - dataUrl: "data:application/pdf;base64,BBB", - }, - ], - text: "check these", - messageID: "msg_multi", - sessionID: "ses_multi", - sessionDirectory: "/repo", - }) - - const files = result.requestParts.filter((part) => part.type === "file" && part.url.startsWith("data:")) - - expect(files).toHaveLength(2) - expect(files.map((part) => (part.type === "file" ? part.filename : ""))).toEqual(["a.png", "b.pdf"]) - }) - - test("deduplicates context files when prompt already includes same path", () => { - const prompt: Prompt = [{ type: "file", path: "src/foo.ts", content: "@src/foo.ts", start: 0, end: 11 }] - - const result = buildRequestParts({ - prompt, - context: [ - { key: "ctx:dup", type: "file", path: "src/foo.ts" }, - { key: "ctx:comment", type: "file", path: "src/foo.ts", comment: "focus here" }, - ], - images: [], - text: "@src/foo.ts", - messageID: "msg_2", - sessionID: "ses_2", - sessionDirectory: "/repo", - }) - - const fooFiles = result.requestParts.filter( - (part) => part.type === "file" && part.url.startsWith("file:///repo/src/foo.ts"), - ) - const synthetic = result.requestParts.filter((part) => part.type === "text" && part.synthetic) - - expect(fooFiles).toHaveLength(2) - expect(synthetic).toHaveLength(1) - }) - - test("adds file parts for @mentions inside comment text", () => { - const result = buildRequestParts({ - prompt: [{ type: "text", content: "look", start: 0, end: 4 }], - context: [ - { - key: "ctx:comment-mention", - type: "file", - path: "src/review.ts", - comment: "Compare with @src/shared.ts and @src/review.ts.", - }, - ], - images: [], - text: "look", - messageID: "msg_comment_mentions", - sessionID: "ses_comment_mentions", - sessionDirectory: "/repo", - }) - - const files = result.requestParts.filter((part) => part.type === "file") - expect(files).toHaveLength(2) - expect(files.some((part) => part.type === "file" && part.url === "file:///repo/src/review.ts")).toBe(true) - expect(files.some((part) => part.type === "file" && part.url === "file:///repo/src/shared.ts")).toBe(true) - }) - - test("handles Windows paths correctly (simulated on macOS)", () => { - const prompt: Prompt = [{ type: "file", path: "src\\foo.ts", content: "@src\\foo.ts", start: 0, end: 11 }] - - const result = buildRequestParts({ - prompt, - context: [], - images: [], - text: "@src\\foo.ts", - messageID: "msg_win_1", - sessionID: "ses_win_1", - sessionDirectory: "D:\\projects\\myapp", // Windows path - }) - - // Should create valid file URLs - const filePart = result.requestParts.find((part) => part.type === "file") - expect(filePart).toBeDefined() - if (filePart?.type === "file") { - // URL should be parseable - expect(() => new URL(filePart.url)).not.toThrow() - // Should not have encoded backslashes in wrong place - expect(filePart.url).not.toContain("%5C") - // Should have normalized to forward slashes - expect(filePart.url).toContain("/src/foo.ts") - } - }) - - test("handles Windows absolute path with special characters", () => { - const prompt: Prompt = [{ type: "file", path: "file#name.txt", content: "@file#name.txt", start: 0, end: 14 }] - - const result = buildRequestParts({ - prompt, - context: [], - images: [], - text: "@file#name.txt", - messageID: "msg_win_2", - sessionID: "ses_win_2", - sessionDirectory: "C:\\Users\\test\\Documents", // Windows path - }) - - const filePart = result.requestParts.find((part) => part.type === "file") - expect(filePart).toBeDefined() - if (filePart?.type === "file") { - // URL should be parseable - expect(() => new URL(filePart.url)).not.toThrow() - // Special chars should be encoded - expect(filePart.url).toContain("file%23name.txt") - // Should have Windows drive letter properly encoded - expect(filePart.url).toMatch(/file:\/\/\/[A-Z]:/) - } - }) - - test("handles Linux absolute paths correctly", () => { - const prompt: Prompt = [{ type: "file", path: "src/app.ts", content: "@src/app.ts", start: 0, end: 10 }] - - const result = buildRequestParts({ - prompt, - context: [], - images: [], - text: "@src/app.ts", - messageID: "msg_linux_1", - sessionID: "ses_linux_1", - sessionDirectory: "/home/user/project", - }) - - const filePart = result.requestParts.find((part) => part.type === "file") - expect(filePart).toBeDefined() - if (filePart?.type === "file") { - // URL should be parseable - expect(() => new URL(filePart.url)).not.toThrow() - // Should be a normal Unix path - expect(filePart.url).toBe("file:///home/user/project/src/app.ts") - } - }) - - test("handles macOS paths correctly", () => { - const prompt: Prompt = [{ type: "file", path: "README.md", content: "@README.md", start: 0, end: 9 }] - - const result = buildRequestParts({ - prompt, - context: [], - images: [], - text: "@README.md", - messageID: "msg_mac_1", - sessionID: "ses_mac_1", - sessionDirectory: "/Users/kelvin/Projects/opencode", - }) - - const filePart = result.requestParts.find((part) => part.type === "file") - expect(filePart).toBeDefined() - if (filePart?.type === "file") { - // URL should be parseable - expect(() => new URL(filePart.url)).not.toThrow() - // Should be a normal Unix path - expect(filePart.url).toBe("file:///Users/kelvin/Projects/opencode/README.md") - } - }) - - test("handles context files with Windows paths", () => { - const prompt: Prompt = [] - - const result = buildRequestParts({ - prompt, - context: [ - { key: "ctx:1", type: "file", path: "src\\utils\\helper.ts" }, - { key: "ctx:2", type: "file", path: "test\\unit.test.ts", comment: "check tests" }, - ], - images: [], - text: "test", - messageID: "msg_win_ctx", - sessionID: "ses_win_ctx", - sessionDirectory: "D:\\workspace\\app", - }) - - const fileParts = result.requestParts.filter((part) => part.type === "file") - expect(fileParts).toHaveLength(2) - - // All file URLs should be valid - fileParts.forEach((part) => { - if (part.type === "file") { - expect(() => new URL(part.url)).not.toThrow() - expect(part.url).not.toContain("%5C") // No encoded backslashes - } - }) - }) - - test("handles absolute Windows paths (user manually specifies full path)", () => { - const prompt: Prompt = [ - { type: "file", path: "D:\\other\\project\\file.ts", content: "@D:\\other\\project\\file.ts", start: 0, end: 25 }, - ] - - const result = buildRequestParts({ - prompt, - context: [], - images: [], - text: "@D:\\other\\project\\file.ts", - messageID: "msg_abs", - sessionID: "ses_abs", - sessionDirectory: "C:\\current\\project", - }) - - const filePart = result.requestParts.find((part) => part.type === "file") - expect(filePart).toBeDefined() - if (filePart?.type === "file") { - // Should handle absolute path that differs from sessionDirectory - expect(() => new URL(filePart.url)).not.toThrow() - expect(filePart.url).toContain("/D:/other/project/file.ts") - } - }) - - test("handles selection with query parameters on Windows", () => { - const prompt: Prompt = [ - { - type: "file", - path: "src\\App.tsx", - content: "@src\\App.tsx", - start: 0, - end: 11, - selection: { startLine: 10, startChar: 0, endLine: 20, endChar: 5 }, - }, - ] - - const result = buildRequestParts({ - prompt, - context: [], - images: [], - text: "@src\\App.tsx", - messageID: "msg_sel", - sessionID: "ses_sel", - sessionDirectory: "C:\\project", - }) - - const filePart = result.requestParts.find((part) => part.type === "file") - expect(filePart).toBeDefined() - if (filePart?.type === "file") { - // Should have query parameters - expect(filePart.url).toContain("?start=10&end=20") - // Should be valid URL - expect(() => new URL(filePart.url)).not.toThrow() - // Query params should parse correctly - const url = new URL(filePart.url) - expect(url.searchParams.get("start")).toBe("10") - expect(url.searchParams.get("end")).toBe("20") - } - }) - - test("handles file paths with dots and special segments on Windows", () => { - const prompt: Prompt = [ - { type: "file", path: "..\\..\\shared\\util.ts", content: "@..\\..\\shared\\util.ts", start: 0, end: 21 }, - ] - - const result = buildRequestParts({ - prompt, - context: [], - images: [], - text: "@..\\..\\shared\\util.ts", - messageID: "msg_dots", - sessionID: "ses_dots", - sessionDirectory: "C:\\projects\\myapp\\src", - }) - - const filePart = result.requestParts.find((part) => part.type === "file") - expect(filePart).toBeDefined() - if (filePart?.type === "file") { - // Should be valid URL - expect(() => new URL(filePart.url)).not.toThrow() - // Should preserve .. segments (backend normalizes) - expect(filePart.url).toContain("/..") - } - }) -}) diff --git a/packages/app/src/components/prompt-input/build-request-parts.ts b/packages/app/src/components/prompt-input/build-request-parts.ts deleted file mode 100644 index 9a62d0b71d1..00000000000 --- a/packages/app/src/components/prompt-input/build-request-parts.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { getFilename } from "@opencode-ai/shared/util/path" -import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@kilocode/sdk/v2/client" -import type { FileSelection } from "@/context/file" -import { encodeFilePath } from "@/context/file/path" -import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt" -import { Identifier } from "@/utils/id" -import { createCommentMetadata, formatCommentNote } from "@/utils/comment-note" - -type PromptRequestPart = (TextPartInput | FilePartInput | AgentPartInput) & { id: string } - -type ContextFile = { - key: string - type: "file" - path: string - selection?: FileSelection - comment?: string - commentID?: string - commentOrigin?: "review" | "file" - preview?: string -} - -type BuildRequestPartsInput = { - prompt: Prompt - context: ContextFile[] - images: ImageAttachmentPart[] - text: string - messageID: string - sessionID: string - sessionDirectory: string -} - -const absolute = (directory: string, path: string) => { - if (path.startsWith("/")) return path - if (/^[A-Za-z]:[\\/]/.test(path) || /^[A-Za-z]:$/.test(path)) return path - if (path.startsWith("\\\\") || path.startsWith("//")) return path - return `${directory.replace(/[\\/]+$/, "")}/${path}` -} - -const fileQuery = (selection: FileSelection | undefined) => - selection ? `?start=${selection.startLine}&end=${selection.endLine}` : "" - -const mention = /(^|[\s([{"'])@(\S+)/g - -const parseCommentMentions = (comment: string) => { - return Array.from(comment.matchAll(mention)).flatMap((match) => { - const path = (match[2] ?? "").replace(/[.,!?;:)}\]"']+$/, "") - if (!path) return [] - return [path] - }) -} - -const isFileAttachment = (part: Prompt[number]): part is FileAttachmentPart => part.type === "file" -const isAgentAttachment = (part: Prompt[number]): part is AgentPart => part.type === "agent" - -const toOptimisticPart = (part: PromptRequestPart, sessionID: string, messageID: string): Part => { - if (part.type === "text") { - return { - id: part.id, - type: "text", - text: part.text, - synthetic: part.synthetic, - ignored: part.ignored, - time: part.time, - metadata: part.metadata, - sessionID, - messageID, - } - } - if (part.type === "file") { - return { - id: part.id, - type: "file", - mime: part.mime, - filename: part.filename, - url: part.url, - source: part.source, - sessionID, - messageID, - } - } - return { - id: part.id, - type: "agent", - name: part.name, - source: part.source, - sessionID, - messageID, - } -} - -export function buildRequestParts(input: BuildRequestPartsInput) { - const requestParts: PromptRequestPart[] = [ - { - id: Identifier.ascending("part"), - type: "text", - text: input.text, - }, - ] - - const files = input.prompt.filter(isFileAttachment).map((attachment) => { - const path = absolute(input.sessionDirectory, attachment.path) - return { - id: Identifier.ascending("part"), - type: "file", - mime: "text/plain", - url: `file://${encodeFilePath(path)}${fileQuery(attachment.selection)}`, - filename: getFilename(attachment.path), - source: { - type: "file", - text: { - value: attachment.content, - start: attachment.start, - end: attachment.end, - }, - path, - }, - } satisfies PromptRequestPart - }) - - const agents = input.prompt.filter(isAgentAttachment).map((attachment) => { - return { - id: Identifier.ascending("part"), - type: "agent", - name: attachment.name, - source: { - value: attachment.content, - start: attachment.start, - end: attachment.end, - }, - } satisfies PromptRequestPart - }) - - const used = new Set(files.map((part) => part.url)) - const context = input.context.flatMap((item) => { - const path = absolute(input.sessionDirectory, item.path) - const url = `file://${encodeFilePath(path)}${fileQuery(item.selection)}` - const comment = item.comment?.trim() - if (!comment && used.has(url)) return [] - used.add(url) - - const filePart = { - id: Identifier.ascending("part"), - type: "file", - mime: "text/plain", - url, - filename: getFilename(item.path), - } satisfies PromptRequestPart - - if (!comment) return [filePart] - - const mentions = parseCommentMentions(comment).flatMap((path) => { - const url = `file://${encodeFilePath(absolute(input.sessionDirectory, path))}` - if (used.has(url)) return [] - used.add(url) - return [ - { - id: Identifier.ascending("part"), - type: "file", - mime: "text/plain", - url, - filename: getFilename(path), - } satisfies PromptRequestPart, - ] - }) - - return [ - { - id: Identifier.ascending("part"), - type: "text", - text: formatCommentNote({ path: item.path, selection: item.selection, comment }), - synthetic: true, - metadata: createCommentMetadata({ - path: item.path, - selection: item.selection, - comment, - preview: item.preview, - origin: item.commentOrigin, - }), - } satisfies PromptRequestPart, - filePart, - ...mentions, - ] - }) - - const images = input.images.map((attachment) => { - return { - id: Identifier.ascending("part"), - type: "file", - mime: attachment.mime, - url: attachment.dataUrl, - filename: attachment.filename, - } satisfies PromptRequestPart - }) - - requestParts.push(...files, ...context, ...agents, ...images) - - return { - requestParts, - optimisticParts: requestParts.map((part) => toOptimisticPart(part, input.sessionID, input.messageID)), - } -} diff --git a/packages/app/src/components/prompt-input/context-items.tsx b/packages/app/src/components/prompt-input/context-items.tsx deleted file mode 100644 index 9f20f1c04b0..00000000000 --- a/packages/app/src/components/prompt-input/context-items.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Component, For, Show } from "solid-js" -import { FileIcon } from "@opencode-ai/ui/file-icon" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/shared/util/path" -import type { ContextItem } from "@/context/prompt" - -type PromptContextItem = ContextItem & { key: string } - -type ContextItemsProps = { - items: PromptContextItem[] - active: (item: PromptContextItem) => boolean - openComment: (item: PromptContextItem) => void - remove: (item: PromptContextItem) => void - t: (key: string) => string -} - -export const PromptContextItems: Component = (props) => { - return ( - 0}> -
- - {(item) => { - const directory = getDirectory(item.path) - const filename = getFilename(item.path) - const label = getFilenameTruncated(item.path, 14) - const selected = props.active(item) - - return ( - - - {directory} - - {filename} - - } - placement="top" - openDelay={2000} - > -
props.openComment(item)} - > -
- -
- {label} - - {(sel) => ( - - {sel().startLine === sel().endLine - ? `:${sel().startLine}` - : `:${sel().startLine}-${sel().endLine}`} - - )} - -
- { - e.stopPropagation() - props.remove(item) - }} - aria-label={props.t("prompt.context.removeFile")} - /> -
- - {(comment) =>
{comment()}
} -
-
-
- ) - }} -
-
-
- ) -} diff --git a/packages/app/src/components/prompt-input/drag-overlay.tsx b/packages/app/src/components/prompt-input/drag-overlay.tsx deleted file mode 100644 index 41962ce536e..00000000000 --- a/packages/app/src/components/prompt-input/drag-overlay.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Component, Show } from "solid-js" -import { Icon } from "@opencode-ai/ui/icon" - -type PromptDragOverlayProps = { - type: "image" | "@mention" | null - label: string -} - -const kindToIcon = { - image: "photo", - "@mention": "link", -} as const - -export const PromptDragOverlay: Component = (props) => { - return ( - -
-
- - {props.label} -
-
-
- ) -} diff --git a/packages/app/src/components/prompt-input/editor-dom.test.ts b/packages/app/src/components/prompt-input/editor-dom.test.ts deleted file mode 100644 index 3088522a59f..00000000000 --- a/packages/app/src/components/prompt-input/editor-dom.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { createTextFragment, getCursorPosition, getNodeLength, getTextLength, setCursorPosition } from "./editor-dom" - -describe("prompt-input editor dom", () => { - test("createTextFragment preserves newlines with consecutive br nodes", () => { - const fragment = createTextFragment("foo\n\nbar") - const container = document.createElement("div") - container.appendChild(fragment) - - expect(container.childNodes.length).toBe(4) - expect(container.childNodes[0]?.textContent).toBe("foo") - expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR") - expect((container.childNodes[2] as HTMLElement).tagName).toBe("BR") - expect(container.childNodes[3]?.textContent).toBe("bar") - }) - - test("createTextFragment keeps trailing newline as terminal break", () => { - const fragment = createTextFragment("foo\n") - const container = document.createElement("div") - container.appendChild(fragment) - - expect(container.childNodes.length).toBe(2) - expect(container.childNodes[0]?.textContent).toBe("foo") - expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR") - }) - - test("createTextFragment avoids break-node explosion for large multiline content", () => { - const content = Array.from({ length: 220 }, () => "line").join("\n") - const fragment = createTextFragment(content) - const container = document.createElement("div") - container.appendChild(fragment) - - expect(container.childNodes.length).toBe(1) - expect(container.childNodes[0]?.nodeType).toBe(Node.TEXT_NODE) - expect(container.textContent).toBe(content) - }) - - test("createTextFragment keeps terminal break in large multiline fallback", () => { - const content = `${Array.from({ length: 220 }, () => "line").join("\n")}\n` - const fragment = createTextFragment(content) - const container = document.createElement("div") - container.appendChild(fragment) - - expect(container.childNodes.length).toBe(2) - expect(container.childNodes[0]?.textContent).toBe(content.slice(0, -1)) - expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR") - }) - - test("length helpers treat breaks as one char and ignore zero-width chars", () => { - const container = document.createElement("div") - container.appendChild(document.createTextNode("ab\u200B")) - container.appendChild(document.createElement("br")) - container.appendChild(document.createTextNode("cd")) - - expect(getNodeLength(container.childNodes[0]!)).toBe(2) - expect(getNodeLength(container.childNodes[1]!)).toBe(1) - expect(getTextLength(container)).toBe(5) - }) - - test("setCursorPosition and getCursorPosition round-trip with pills and breaks", () => { - const container = document.createElement("div") - const pill = document.createElement("span") - pill.dataset.type = "file" - pill.textContent = "@file" - container.appendChild(document.createTextNode("ab")) - container.appendChild(pill) - container.appendChild(document.createElement("br")) - container.appendChild(document.createTextNode("cd")) - document.body.appendChild(container) - - setCursorPosition(container, 2) - expect(getCursorPosition(container)).toBe(2) - - setCursorPosition(container, 7) - expect(getCursorPosition(container)).toBe(7) - - setCursorPosition(container, 8) - expect(getCursorPosition(container)).toBe(8) - - container.remove() - }) - - test("setCursorPosition and getCursorPosition round-trip across blank lines", () => { - const container = document.createElement("div") - container.appendChild(document.createTextNode("a")) - container.appendChild(document.createElement("br")) - container.appendChild(document.createElement("br")) - container.appendChild(document.createTextNode("b")) - document.body.appendChild(container) - - setCursorPosition(container, 2) - expect(getCursorPosition(container)).toBe(2) - - setCursorPosition(container, 3) - expect(getCursorPosition(container)).toBe(3) - - container.remove() - }) -}) diff --git a/packages/app/src/components/prompt-input/editor-dom.ts b/packages/app/src/components/prompt-input/editor-dom.ts deleted file mode 100644 index 8575140d7d5..00000000000 --- a/packages/app/src/components/prompt-input/editor-dom.ts +++ /dev/null @@ -1,148 +0,0 @@ -const MAX_BREAKS = 200 - -export function createTextFragment(content: string): DocumentFragment { - const fragment = document.createDocumentFragment() - let breaks = 0 - for (const char of content) { - if (char !== "\n") continue - breaks += 1 - if (breaks > MAX_BREAKS) { - const tail = content.endsWith("\n") - const text = tail ? content.slice(0, -1) : content - if (text) fragment.appendChild(document.createTextNode(text)) - if (tail) fragment.appendChild(document.createElement("br")) - return fragment - } - } - - const segments = content.split("\n") - segments.forEach((segment, index) => { - if (segment) { - fragment.appendChild(document.createTextNode(segment)) - } - if (index < segments.length - 1) { - fragment.appendChild(document.createElement("br")) - } - }) - return fragment -} - -export function getNodeLength(node: Node): number { - if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1 - return (node.textContent ?? "").replace(/\u200B/g, "").length -} - -export function getTextLength(node: Node): number { - if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? "").replace(/\u200B/g, "").length - if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1 - let length = 0 - for (const child of Array.from(node.childNodes)) { - length += getTextLength(child) - } - return length -} - -export function getCursorPosition(parent: HTMLElement): number { - const selection = window.getSelection() - if (!selection || selection.rangeCount === 0) return 0 - const range = selection.getRangeAt(0) - if (!parent.contains(range.startContainer)) return 0 - const preCaretRange = range.cloneRange() - preCaretRange.selectNodeContents(parent) - preCaretRange.setEnd(range.startContainer, range.startOffset) - return getTextLength(preCaretRange.cloneContents()) -} - -export function setCursorPosition(parent: HTMLElement, position: number) { - let remaining = position - let node = parent.firstChild - while (node) { - const length = getNodeLength(node) - const isText = node.nodeType === Node.TEXT_NODE - const isPill = - node.nodeType === Node.ELEMENT_NODE && - ((node as HTMLElement).dataset.type === "file" || (node as HTMLElement).dataset.type === "agent") - const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR" - - if (isText && remaining <= length) { - const range = document.createRange() - const selection = window.getSelection() - range.setStart(node, remaining) - range.collapse(true) - selection?.removeAllRanges() - selection?.addRange(range) - return - } - - if ((isPill || isBreak) && remaining <= length) { - const range = document.createRange() - const selection = window.getSelection() - if (remaining === 0) { - range.setStartBefore(node) - } - if (remaining > 0 && isPill) { - range.setStartAfter(node) - } - if (remaining > 0 && isBreak) { - const next = node.nextSibling - if (next && next.nodeType === Node.TEXT_NODE) { - range.setStart(next, 0) - } - if (!next || next.nodeType !== Node.TEXT_NODE) { - range.setStartAfter(node) - } - } - range.collapse(true) - selection?.removeAllRanges() - selection?.addRange(range) - return - } - - remaining -= length - node = node.nextSibling - } - - const fallbackRange = document.createRange() - const fallbackSelection = window.getSelection() - const last = parent.lastChild - if (last && last.nodeType === Node.TEXT_NODE) { - const len = last.textContent ? last.textContent.length : 0 - fallbackRange.setStart(last, len) - } - if (!last || last.nodeType !== Node.TEXT_NODE) { - fallbackRange.selectNodeContents(parent) - } - fallbackRange.collapse(false) - fallbackSelection?.removeAllRanges() - fallbackSelection?.addRange(fallbackRange) -} - -export function setRangeEdge(parent: HTMLElement, range: Range, edge: "start" | "end", offset: number) { - let remaining = offset - const nodes = Array.from(parent.childNodes) - - for (const node of nodes) { - const length = getNodeLength(node) - const isText = node.nodeType === Node.TEXT_NODE - const isPill = - node.nodeType === Node.ELEMENT_NODE && - ((node as HTMLElement).dataset.type === "file" || (node as HTMLElement).dataset.type === "agent") - const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR" - - if (isText && remaining <= length) { - if (edge === "start") range.setStart(node, remaining) - if (edge === "end") range.setEnd(node, remaining) - return - } - - if ((isPill || isBreak) && remaining <= length) { - if (edge === "start" && remaining === 0) range.setStartBefore(node) - if (edge === "start" && remaining > 0) range.setStartAfter(node) - if (edge === "end" && remaining === 0) range.setEndBefore(node) - if (edge === "end" && remaining > 0) range.setEndAfter(node) - return - } - - remaining -= length - } -} diff --git a/packages/app/src/components/prompt-input/files.ts b/packages/app/src/components/prompt-input/files.ts deleted file mode 100644 index eae8af03d9f..00000000000 --- a/packages/app/src/components/prompt-input/files.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ACCEPTED_FILE_TYPES, ACCEPTED_IMAGE_TYPES } from "@/constants/file-picker" - -export { ACCEPTED_FILE_TYPES } - -const IMAGE_MIMES = new Set(ACCEPTED_IMAGE_TYPES) -const IMAGE_EXTS = new Map([ - ["gif", "image/gif"], - ["jpeg", "image/jpeg"], - ["jpg", "image/jpeg"], - ["png", "image/png"], - ["webp", "image/webp"], -]) -const TEXT_MIMES = new Set([ - "application/json", - "application/ld+json", - "application/toml", - "application/x-toml", - "application/x-yaml", - "application/xml", - "application/yaml", -]) - -const SAMPLE = 4096 - -function kind(type: string) { - return type.split(";", 1)[0]?.trim().toLowerCase() ?? "" -} - -function ext(name: string) { - const idx = name.lastIndexOf(".") - if (idx === -1) return "" - return name.slice(idx + 1).toLowerCase() -} - -function textMime(type: string) { - if (!type) return false - if (type.startsWith("text/")) return true - if (TEXT_MIMES.has(type)) return true - if (type.endsWith("+json")) return true - return type.endsWith("+xml") -} - -function textBytes(bytes: Uint8Array) { - if (bytes.length === 0) return true - let count = 0 - for (const byte of bytes) { - if (byte === 0) return false - if (byte < 9 || (byte > 13 && byte < 32)) count += 1 - } - return count / bytes.length <= 0.3 -} - -export async function attachmentMime(file: File) { - const type = kind(file.type) - if (IMAGE_MIMES.has(type)) return type - if (type === "application/pdf") return type - - const suffix = ext(file.name) - const fallback = IMAGE_EXTS.get(suffix) ?? (suffix === "pdf" ? "application/pdf" : undefined) - if ((!type || type === "application/octet-stream") && fallback) return fallback - - if (textMime(type)) return "text/plain" - const bytes = new Uint8Array(await file.slice(0, SAMPLE).arrayBuffer()) - if (!textBytes(bytes)) return - return "text/plain" -} diff --git a/packages/app/src/components/prompt-input/history.test.ts b/packages/app/src/components/prompt-input/history.test.ts deleted file mode 100644 index 5e9c2c66ead..00000000000 --- a/packages/app/src/components/prompt-input/history.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { Prompt } from "@/context/prompt" -import { - canNavigateHistoryAtCursor, - clonePromptParts, - normalizePromptHistoryEntry, - navigatePromptHistory, - prependHistoryEntry, - promptLength, - type PromptHistoryComment, -} from "./history" - -const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }] - -const text = (value: string): Prompt => [{ type: "text", content: value, start: 0, end: value.length }] -const comment = (id: string, value = "note"): PromptHistoryComment => ({ - id, - path: "src/a.ts", - selection: { start: 2, end: 4 }, - comment: value, - time: 1, - origin: "review", - preview: "const a = 1", -}) - -describe("prompt-input history", () => { - test("prependHistoryEntry skips empty prompt and deduplicates consecutive entries", () => { - const first = prependHistoryEntry([], DEFAULT_PROMPT) - expect(first).toEqual([]) - - const commentsOnly = prependHistoryEntry([], DEFAULT_PROMPT, [comment("c1")]) - expect(commentsOnly).toHaveLength(1) - - const withOne = prependHistoryEntry([], text("hello")) - expect(withOne).toHaveLength(1) - - const deduped = prependHistoryEntry(withOne, text("hello")) - expect(deduped).toBe(withOne) - - const dedupedComments = prependHistoryEntry(commentsOnly, DEFAULT_PROMPT, [comment("c1")]) - expect(dedupedComments).toBe(commentsOnly) - }) - - test("navigatePromptHistory restores saved prompt when moving down from newest", () => { - const entries = [text("third"), text("second"), text("first")] - const up = navigatePromptHistory({ - direction: "up", - entries, - historyIndex: -1, - currentPrompt: text("draft"), - currentComments: [comment("draft")], - savedPrompt: null, - }) - expect(up.handled).toBe(true) - if (!up.handled) throw new Error("expected handled") - expect(up.historyIndex).toBe(0) - expect(up.cursor).toBe("start") - expect(up.entry.comments).toEqual([]) - - const down = navigatePromptHistory({ - direction: "down", - entries, - historyIndex: up.historyIndex, - currentPrompt: text("ignored"), - currentComments: [], - savedPrompt: up.savedPrompt, - }) - expect(down.handled).toBe(true) - if (!down.handled) throw new Error("expected handled") - expect(down.historyIndex).toBe(-1) - expect(down.entry.prompt[0]?.type === "text" ? down.entry.prompt[0].content : "").toBe("draft") - expect(down.entry.comments).toEqual([comment("draft")]) - }) - - test("navigatePromptHistory keeps entry comments when moving through history", () => { - const entries = [ - { - prompt: text("with comment"), - comments: [comment("c1")], - }, - ] - - const up = navigatePromptHistory({ - direction: "up", - entries, - historyIndex: -1, - currentPrompt: text("draft"), - currentComments: [], - savedPrompt: null, - }) - - expect(up.handled).toBe(true) - if (!up.handled) throw new Error("expected handled") - expect(up.entry.prompt[0]?.type === "text" ? up.entry.prompt[0].content : "").toBe("with comment") - expect(up.entry.comments).toEqual([comment("c1")]) - }) - - test("normalizePromptHistoryEntry supports legacy prompt arrays", () => { - const entry = normalizePromptHistoryEntry(text("legacy")) - expect(entry.prompt[0]?.type === "text" ? entry.prompt[0].content : "").toBe("legacy") - expect(entry.comments).toEqual([]) - }) - - test("helpers clone prompt and count text content length", () => { - const original: Prompt = [ - { type: "text", content: "one", start: 0, end: 3 }, - { - type: "file", - path: "src/a.ts", - content: "@src/a.ts", - start: 3, - end: 12, - selection: { startLine: 1, startChar: 1, endLine: 2, endChar: 1 }, - }, - { type: "image", id: "1", filename: "img.png", mime: "image/png", dataUrl: "data:image/png;base64,abc" }, - ] - const copy = clonePromptParts(original) - expect(copy).not.toBe(original) - expect(promptLength(copy)).toBe(12) - if (copy[1]?.type !== "file") throw new Error("expected file") - copy[1].selection!.startLine = 9 - if (original[1]?.type !== "file") throw new Error("expected file") - expect(original[1].selection?.startLine).toBe(1) - }) - - test("canNavigateHistoryAtCursor only allows prompt boundaries", () => { - const value = "a\nb\nc" - - expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(false) - expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false) - - expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false) - expect(canNavigateHistoryAtCursor("down", value, 2)).toBe(false) - - expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false) - expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true) - - expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(false) - expect(canNavigateHistoryAtCursor("down", "abc", 3)).toBe(true) - expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(false) - expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(false) - - expect(canNavigateHistoryAtCursor("up", "", 0)).toBe(true) - expect(canNavigateHistoryAtCursor("down", "", 0)).toBe(true) - - expect(canNavigateHistoryAtCursor("up", "abc", 0, true)).toBe(true) - expect(canNavigateHistoryAtCursor("up", "abc", 3, true)).toBe(true) - expect(canNavigateHistoryAtCursor("down", "abc", 0, true)).toBe(true) - expect(canNavigateHistoryAtCursor("down", "abc", 3, true)).toBe(true) - expect(canNavigateHistoryAtCursor("up", "abc", 1, true)).toBe(false) - expect(canNavigateHistoryAtCursor("down", "abc", 1, true)).toBe(false) - }) -}) diff --git a/packages/app/src/components/prompt-input/history.ts b/packages/app/src/components/prompt-input/history.ts deleted file mode 100644 index 79e8abc0d9e..00000000000 --- a/packages/app/src/components/prompt-input/history.ts +++ /dev/null @@ -1,256 +0,0 @@ -import type { Prompt } from "@/context/prompt" -import type { SelectedLineRange } from "@/context/file" - -const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }] - -export const MAX_HISTORY = 100 - -export type PromptHistoryComment = { - id: string - path: string - selection: SelectedLineRange - comment: string - time: number - origin?: "review" | "file" - preview?: string -} - -export type PromptHistoryEntry = { - prompt: Prompt - comments: PromptHistoryComment[] -} - -export type PromptHistoryStoredEntry = Prompt | PromptHistoryEntry - -export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number, inHistory = false) { - const position = Math.max(0, Math.min(cursor, text.length)) - const atStart = position === 0 - const atEnd = position === text.length - if (inHistory) return atStart || atEnd - if (direction === "up") return position === 0 && text.length === 0 - return position === text.length -} - -export function clonePromptParts(prompt: Prompt): Prompt { - return prompt.map((part) => { - if (part.type === "text") return { ...part } - if (part.type === "image") return { ...part } - if (part.type === "agent") return { ...part } - return { - ...part, - selection: part.selection ? { ...part.selection } : undefined, - } - }) -} - -function cloneSelection(selection: SelectedLineRange): SelectedLineRange { - return { - start: selection.start, - end: selection.end, - ...(selection.side ? { side: selection.side } : {}), - ...(selection.endSide ? { endSide: selection.endSide } : {}), - } -} - -export function clonePromptHistoryComments(comments: PromptHistoryComment[]) { - return comments.map((comment) => ({ - ...comment, - selection: cloneSelection(comment.selection), - })) -} - -export function normalizePromptHistoryEntry(entry: PromptHistoryStoredEntry): PromptHistoryEntry { - if (Array.isArray(entry)) { - return { - prompt: clonePromptParts(entry), - comments: [], - } - } - return { - prompt: clonePromptParts(entry.prompt), - comments: clonePromptHistoryComments(entry.comments), - } -} - -export function promptLength(prompt: Prompt) { - return prompt.reduce((len, part) => len + ("content" in part ? part.content.length : 0), 0) -} - -export function prependHistoryEntry( - entries: PromptHistoryStoredEntry[], - prompt: Prompt, - comments: PromptHistoryComment[] = [], - max = MAX_HISTORY, -) { - const text = prompt - .map((part) => ("content" in part ? part.content : "")) - .join("") - .trim() - const hasImages = prompt.some((part) => part.type === "image") - const hasComments = comments.some((comment) => !!comment.comment.trim()) - if (!text && !hasImages && !hasComments) return entries - - const entry = { - prompt: clonePromptParts(prompt), - comments: clonePromptHistoryComments(comments), - } satisfies PromptHistoryEntry - const last = entries[0] - if (last && isPromptEqual(last, entry)) return entries - return [entry, ...entries].slice(0, max) -} - -function isCommentEqual(commentA: PromptHistoryComment, commentB: PromptHistoryComment) { - return ( - commentA.path === commentB.path && - commentA.comment === commentB.comment && - commentA.origin === commentB.origin && - commentA.preview === commentB.preview && - commentA.selection.start === commentB.selection.start && - commentA.selection.end === commentB.selection.end && - commentA.selection.side === commentB.selection.side && - commentA.selection.endSide === commentB.selection.endSide - ) -} - -function isPromptEqual(promptA: PromptHistoryStoredEntry, promptB: PromptHistoryStoredEntry) { - const entryA = normalizePromptHistoryEntry(promptA) - const entryB = normalizePromptHistoryEntry(promptB) - if (entryA.prompt.length !== entryB.prompt.length) return false - for (let i = 0; i < entryA.prompt.length; i++) { - const partA = entryA.prompt[i] - const partB = entryB.prompt[i] - if (partA.type !== partB.type) return false - if (partA.type === "text" && partA.content !== (partB.type === "text" ? partB.content : "")) return false - if (partA.type === "file") { - if (partA.path !== (partB.type === "file" ? partB.path : "")) return false - const a = partA.selection - const b = partB.type === "file" ? partB.selection : undefined - const sameSelection = - (!a && !b) || - (!!a && - !!b && - a.startLine === b.startLine && - a.startChar === b.startChar && - a.endLine === b.endLine && - a.endChar === b.endChar) - if (!sameSelection) return false - } - if (partA.type === "agent" && partA.name !== (partB.type === "agent" ? partB.name : "")) return false - if (partA.type === "image" && partA.id !== (partB.type === "image" ? partB.id : "")) return false - } - if (entryA.comments.length !== entryB.comments.length) return false - for (let i = 0; i < entryA.comments.length; i++) { - const commentA = entryA.comments[i] - const commentB = entryB.comments[i] - if (!commentA || !commentB || !isCommentEqual(commentA, commentB)) return false - } - return true -} - -type HistoryNavInput = { - direction: "up" | "down" - entries: PromptHistoryStoredEntry[] - historyIndex: number - currentPrompt: Prompt - currentComments: PromptHistoryComment[] - savedPrompt: PromptHistoryEntry | null -} - -type HistoryNavResult = - | { - handled: false - historyIndex: number - savedPrompt: PromptHistoryEntry | null - } - | { - handled: true - historyIndex: number - savedPrompt: PromptHistoryEntry | null - entry: PromptHistoryEntry - cursor: "start" | "end" - } - -export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult { - if (input.direction === "up") { - if (input.entries.length === 0) { - return { - handled: false, - historyIndex: input.historyIndex, - savedPrompt: input.savedPrompt, - } - } - - if (input.historyIndex === -1) { - const entry = normalizePromptHistoryEntry(input.entries[0]) - return { - handled: true, - historyIndex: 0, - savedPrompt: { - prompt: clonePromptParts(input.currentPrompt), - comments: clonePromptHistoryComments(input.currentComments), - }, - entry, - cursor: "start", - } - } - - if (input.historyIndex < input.entries.length - 1) { - const next = input.historyIndex + 1 - const entry = normalizePromptHistoryEntry(input.entries[next]) - return { - handled: true, - historyIndex: next, - savedPrompt: input.savedPrompt, - entry, - cursor: "start", - } - } - - return { - handled: false, - historyIndex: input.historyIndex, - savedPrompt: input.savedPrompt, - } - } - - if (input.historyIndex > 0) { - const next = input.historyIndex - 1 - const entry = normalizePromptHistoryEntry(input.entries[next]) - return { - handled: true, - historyIndex: next, - savedPrompt: input.savedPrompt, - entry, - cursor: "end", - } - } - - if (input.historyIndex === 0) { - if (input.savedPrompt) { - return { - handled: true, - historyIndex: -1, - savedPrompt: null, - entry: input.savedPrompt, - cursor: "end", - } - } - - return { - handled: true, - historyIndex: -1, - savedPrompt: null, - entry: { - prompt: DEFAULT_PROMPT, - comments: [], - }, - cursor: "end", - } - } - - return { - handled: false, - historyIndex: input.historyIndex, - savedPrompt: input.savedPrompt, - } -} diff --git a/packages/app/src/components/prompt-input/image-attachments.tsx b/packages/app/src/components/prompt-input/image-attachments.tsx deleted file mode 100644 index dd8138e5a4f..00000000000 --- a/packages/app/src/components/prompt-input/image-attachments.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Component, For, Show } from "solid-js" -import { Icon } from "@opencode-ai/ui/icon" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import type { ImageAttachmentPart } from "@/context/prompt" - -type PromptImageAttachmentsProps = { - attachments: ImageAttachmentPart[] - onOpen: (attachment: ImageAttachmentPart) => void - onRemove: (id: string) => void - removeLabel: string -} - -const fallbackClass = "size-16 rounded-md bg-surface-base flex items-center justify-center border border-border-base" -const imageClass = - "size-16 rounded-md object-cover border border-border-base hover:border-border-strong-base transition-colors" -const removeClass = - "absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-stronger-non-alpha border border-border-base flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-surface-raised-base-hover" -const nameClass = "absolute bottom-0 left-0 right-0 px-1 py-0.5 bg-black/50 rounded-b-md" - -export const PromptImageAttachments: Component = (props) => { - return ( - 0}> -
- - {(attachment) => ( - -
- - -
- } - > - {attachment.filename} props.onOpen(attachment)} - /> - - -
- {attachment.filename} -
-
- - )} - -
- - ) -} diff --git a/packages/app/src/components/prompt-input/paste.ts b/packages/app/src/components/prompt-input/paste.ts deleted file mode 100644 index 6787d503090..00000000000 --- a/packages/app/src/components/prompt-input/paste.ts +++ /dev/null @@ -1,24 +0,0 @@ -const LARGE_PASTE_CHARS = 8000 -const LARGE_PASTE_BREAKS = 120 - -function largePaste(text: string) { - if (text.length >= LARGE_PASTE_CHARS) return true - let breaks = 0 - for (const char of text) { - if (char !== "\n") continue - breaks += 1 - if (breaks >= LARGE_PASTE_BREAKS) return true - } - return false -} - -export function normalizePaste(text: string) { - if (!text.includes("\r")) return text - return text.replace(/\r\n?/g, "\n") -} - -export function pasteMode(text: string) { - if (largePaste(text)) return "manual" - if (text.includes("\n") || text.includes("\r")) return "manual" - return "native" -} diff --git a/packages/app/src/components/prompt-input/placeholder.test.ts b/packages/app/src/components/prompt-input/placeholder.test.ts deleted file mode 100644 index d4caead0d2e..00000000000 --- a/packages/app/src/components/prompt-input/placeholder.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { promptPlaceholder } from "./placeholder" - -describe("promptPlaceholder", () => { - const t = (key: string, params?: Record) => `${key}${params?.example ? `:${params.example}` : ""}` - - test("returns shell placeholder in shell mode", () => { - const value = promptPlaceholder({ - mode: "shell", - commentCount: 0, - example: "example", - suggest: true, - t, - }) - expect(value).toBe("prompt.placeholder.shell:example") - }) - - test("returns summarize placeholders for comment context", () => { - expect(promptPlaceholder({ mode: "normal", commentCount: 1, example: "example", suggest: true, t })).toBe( - "prompt.placeholder.summarizeComment", - ) - expect(promptPlaceholder({ mode: "normal", commentCount: 2, example: "example", suggest: true, t })).toBe( - "prompt.placeholder.summarizeComments", - ) - }) - - test("returns default placeholder with example when suggestions enabled", () => { - const value = promptPlaceholder({ - mode: "normal", - commentCount: 0, - example: "translated-example", - suggest: true, - t, - }) - expect(value).toBe("prompt.placeholder.normal:translated-example") - }) - - test("returns simple placeholder when suggestions disabled", () => { - const value = promptPlaceholder({ - mode: "normal", - commentCount: 0, - example: "translated-example", - suggest: false, - t, - }) - expect(value).toBe("prompt.placeholder.simple") - }) -}) diff --git a/packages/app/src/components/prompt-input/placeholder.ts b/packages/app/src/components/prompt-input/placeholder.ts deleted file mode 100644 index 6669f136147..00000000000 --- a/packages/app/src/components/prompt-input/placeholder.ts +++ /dev/null @@ -1,15 +0,0 @@ -type PromptPlaceholderInput = { - mode: "normal" | "shell" - commentCount: number - example: string - suggest: boolean - t: (key: string, params?: Record) => string -} - -export function promptPlaceholder(input: PromptPlaceholderInput) { - if (input.mode === "shell") return input.t("prompt.placeholder.shell", { example: input.example }) - if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments") - if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment") - if (!input.suggest) return input.t("prompt.placeholder.simple") - return input.t("prompt.placeholder.normal", { example: input.example }) -} diff --git a/packages/app/src/components/prompt-input/slash-popover.tsx b/packages/app/src/components/prompt-input/slash-popover.tsx deleted file mode 100644 index 0c8c9592349..00000000000 --- a/packages/app/src/components/prompt-input/slash-popover.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { Component, For, Match, Show, Switch } from "solid-js" -import { FileIcon } from "@opencode-ai/ui/file-icon" -import { Icon } from "@opencode-ai/ui/icon" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" - -export type AtOption = - | { type: "agent"; name: string; display: string } - | { type: "file"; path: string; display: string; recent?: boolean } - -export interface SlashCommand { - id: string - trigger: string - title: string - description?: string - keybind?: string - type: "builtin" | "custom" - source?: "command" | "mcp" | "skill" -} - -type PromptPopoverProps = { - popover: "at" | "slash" | null - setSlashPopoverRef: (el: HTMLDivElement) => void - atFlat: AtOption[] - atActive?: string - atKey: (item: AtOption) => string - setAtActive: (id: string) => void - onAtSelect: (item: AtOption) => void - slashFlat: SlashCommand[] - slashActive?: string - setSlashActive: (id: string) => void - onSlashSelect: (item: SlashCommand) => void - commandKeybind: (id: string) => string | undefined - t: (key: string) => string -} - -export const PromptPopover: Component = (props) => { - return ( - -
{ - if (props.popover === "slash") props.setSlashPopoverRef(el) - }} - class="absolute inset-x-0 -top-2 -translate-y-full origin-bottom-left max-h-80 min-h-10 - overflow-auto no-scrollbar flex flex-col p-2 rounded-[12px] - bg-surface-raised-stronger-non-alpha shadow-[var(--shadow-lg-border-base)]" - onMouseDown={(e) => e.preventDefault()} - > - - - 0} - fallback={
{props.t("prompt.popover.emptyResults")}
} - > - - {(item) => { - const key = props.atKey(item) - - if (item.type === "agent") { - return ( - - ) - } - - const isDirectory = item.path.endsWith("/") - const directory = isDirectory ? item.path : getDirectory(item.path) - const filename = isDirectory ? "" : getFilename(item.path) - - return ( - - ) - }} - -
-
- - 0} - fallback={
{props.t("prompt.popover.emptyCommands")}
} - > - - {(cmd) => ( - - )} - -
-
-
-
-
- ) -} diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts deleted file mode 100644 index e1b959e8dd7..00000000000 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test" -import type { Prompt } from "@/context/prompt" - -let createPromptSubmit: typeof import("./submit").createPromptSubmit - -const createdClients: string[] = [] -const createdSessions: string[] = [] -const enabledAutoAccept: Array<{ sessionID: string; directory: string }> = [] -const optimistic: Array<{ - directory?: string - sessionID?: string - message: { - agent: string - model: { providerID: string; modelID: string } - variant?: string - } -}> = [] -const optimisticSeeded: boolean[] = [] -const storedSessions: Record> = {} -const promoted: Array<{ directory: string; sessionID: string }> = [] -const sentShell: string[] = [] -const syncedDirectories: string[] = [] - -let params: { id?: string } = {} -let selected = "/repo/worktree-a" -let variant: string | undefined - -const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }] - -const clientFor = (directory: string) => { - createdClients.push(directory) - return { - session: { - create: async () => { - createdSessions.push(directory) - return { - data: { - id: `session-${createdSessions.length}`, - title: `New session ${createdSessions.length}`, - }, - } - }, - shell: async () => { - sentShell.push(directory) - return { data: undefined } - }, - prompt: async () => ({ data: undefined }), - promptAsync: async () => ({ data: undefined }), - command: async () => ({ data: undefined }), - abort: async () => ({ data: undefined }), - }, - worktree: { - create: async () => ({ data: { directory: `${directory}/new` } }), - }, - } -} - -beforeAll(async () => { - const rootClient = clientFor("/repo/main") - - mock.module("@solidjs/router", () => ({ - useNavigate: () => () => undefined, - useParams: () => params, - })) - - mock.module("@kilocode/sdk/v2/client", () => ({ - createKiloClient: (input: { directory: string }) => { - createdClients.push(input.directory) - return clientFor(input.directory) - }, - })) - - mock.module("@opencode-ai/ui/toast", () => ({ - showToast: () => 0, - })) - - mock.module("@opencode-ai/shared/util/encode", () => ({ - base64Encode: (value: string) => value, - })) - - mock.module("@/context/local", () => ({ - useLocal: () => ({ - model: { - current: () => ({ id: "model", provider: { id: "provider" } }), - variant: { current: () => variant }, - }, - agent: { - current: () => ({ name: "agent" }), - }, - session: { - promote(directory: string, sessionID: string) { - promoted.push({ directory, sessionID }) - }, - }, - }), - })) - - mock.module("@/context/permission", () => ({ - usePermission: () => ({ - enableAutoAccept(sessionID: string, directory: string) { - enabledAutoAccept.push({ sessionID, directory }) - }, - }), - })) - - mock.module("@/context/prompt", () => ({ - usePrompt: () => ({ - current: () => promptValue, - reset: () => undefined, - set: () => undefined, - context: { - add: () => undefined, - remove: () => undefined, - items: () => [], - }, - }), - })) - - mock.module("@/context/layout", () => ({ - useLayout: () => ({ - handoff: { - setTabs: () => undefined, - }, - }), - })) - - mock.module("@/context/sdk", () => ({ - useSDK: () => { - const sdk = { - directory: "/repo/main", - client: rootClient, - url: "http://localhost:4096", - createClient(opts: any) { - return clientFor(opts.directory) - }, - } - return sdk - }, - })) - - mock.module("@/context/sync", () => ({ - useSync: () => ({ - data: { command: [] }, - session: { - optimistic: { - add: (value: { - directory?: string - sessionID?: string - message: { agent: string; model: { providerID: string; modelID: string; variant?: string } } - }) => { - optimistic.push(value) - optimisticSeeded.push( - !!value.directory && - !!value.sessionID && - !!storedSessions[value.directory]?.find((item) => item.id === value.sessionID)?.title, - ) - }, - remove: () => undefined, - }, - }, - set: () => undefined, - }), - })) - - mock.module("@/context/global-sync", () => ({ - useGlobalSync: () => ({ - child: (directory: string) => { - syncedDirectories.push(directory) - storedSessions[directory] ??= [] - return [ - { session: storedSessions[directory] }, - (...args: unknown[]) => { - if (args[0] !== "session") return - const next = args[1] - if (typeof next === "function") { - storedSessions[directory] = next(storedSessions[directory]) as Array<{ id: string; title?: string }> - return - } - if (Array.isArray(next)) { - storedSessions[directory] = next as Array<{ id: string; title?: string }> - } - }, - ] - }, - }), - })) - - mock.module("@/context/platform", () => ({ - usePlatform: () => ({ - fetch: fetch, - }), - })) - - mock.module("@/context/language", () => ({ - useLanguage: () => ({ - t: (key: string) => key, - }), - })) - - const mod = await import("./submit") - createPromptSubmit = mod.createPromptSubmit -}) - -beforeEach(() => { - createdClients.length = 0 - createdSessions.length = 0 - enabledAutoAccept.length = 0 - optimistic.length = 0 - optimisticSeeded.length = 0 - promoted.length = 0 - params = {} - sentShell.length = 0 - syncedDirectories.length = 0 - selected = "/repo/worktree-a" - variant = undefined - for (const key of Object.keys(storedSessions)) delete storedSessions[key] -}) - -describe("prompt submit worktree selection", () => { - test("reads the latest worktree accessor value per submit", async () => { - const submit = createPromptSubmit({ - info: () => undefined, - imageAttachments: () => [], - commentCount: () => 0, - autoAccept: () => false, - mode: () => "shell", - working: () => false, - editor: () => undefined, - queueScroll: () => undefined, - promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), - addToHistory: () => undefined, - resetHistoryNavigation: () => undefined, - setMode: () => undefined, - setPopover: () => undefined, - newSessionWorktree: () => selected, - onNewSessionWorktreeReset: () => undefined, - onSubmit: () => undefined, - }) - - const event = { preventDefault: () => undefined } as unknown as Event - - await submit.handleSubmit(event) - selected = "/repo/worktree-b" - await submit.handleSubmit(event) - - expect(createdClients).toEqual(["/repo/worktree-a", "/repo/worktree-b"]) - expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"]) - expect(sentShell).toEqual(["/repo/worktree-a", "/repo/worktree-b"]) - expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"]) - expect(promoted).toEqual([ - { directory: "/repo/worktree-a", sessionID: "session-1" }, - { directory: "/repo/worktree-b", sessionID: "session-2" }, - ]) - expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"]) - }) - - test("applies auto-accept to newly created sessions", async () => { - const submit = createPromptSubmit({ - info: () => undefined, - imageAttachments: () => [], - commentCount: () => 0, - autoAccept: () => true, - mode: () => "shell", - working: () => false, - editor: () => undefined, - queueScroll: () => undefined, - promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), - addToHistory: () => undefined, - resetHistoryNavigation: () => undefined, - setMode: () => undefined, - setPopover: () => undefined, - newSessionWorktree: () => selected, - onNewSessionWorktreeReset: () => undefined, - onSubmit: () => undefined, - }) - - const event = { preventDefault: () => undefined } as unknown as Event - - await submit.handleSubmit(event) - - expect(enabledAutoAccept).toEqual([{ sessionID: "session-1", directory: "/repo/worktree-a" }]) - }) - - test("includes the selected variant on optimistic prompts", async () => { - params = { id: "session-1" } - variant = "high" - - const submit = createPromptSubmit({ - info: () => ({ id: "session-1" }), - imageAttachments: () => [], - commentCount: () => 0, - autoAccept: () => false, - mode: () => "normal", - working: () => false, - editor: () => undefined, - queueScroll: () => undefined, - promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), - addToHistory: () => undefined, - resetHistoryNavigation: () => undefined, - setMode: () => undefined, - setPopover: () => undefined, - onSubmit: () => undefined, - }) - - const event = { preventDefault: () => undefined } as unknown as Event - - await submit.handleSubmit(event) - - expect(optimistic).toHaveLength(1) - expect(optimistic[0]).toMatchObject({ - message: { - agent: "agent", - model: { providerID: "provider", modelID: "model", variant: "high" }, - }, - }) - }) - - test("seeds new sessions before optimistic prompts are added", async () => { - const submit = createPromptSubmit({ - info: () => undefined, - imageAttachments: () => [], - commentCount: () => 0, - autoAccept: () => false, - mode: () => "normal", - working: () => false, - editor: () => undefined, - queueScroll: () => undefined, - promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), - addToHistory: () => undefined, - resetHistoryNavigation: () => undefined, - setMode: () => undefined, - setPopover: () => undefined, - newSessionWorktree: () => selected, - onNewSessionWorktreeReset: () => undefined, - onSubmit: () => undefined, - }) - - const event = { preventDefault: () => undefined } as unknown as Event - - await submit.handleSubmit(event) - - expect(storedSessions["/repo/worktree-a"]).toEqual([{ id: "session-1", title: "New session 1" }]) - expect(optimisticSeeded).toEqual([true]) - }) -}) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts deleted file mode 100644 index fdd2889efa3..00000000000 --- a/packages/app/src/components/prompt-input/submit.ts +++ /dev/null @@ -1,584 +0,0 @@ -import type { Message, Session } from "@kilocode/sdk/v2/client" -import { showToast } from "@opencode-ai/ui/toast" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { Binary } from "@opencode-ai/shared/util/binary" -import { useNavigate, useParams } from "@solidjs/router" -import { batch, type Accessor } from "solid-js" -import type { FileSelection } from "@/context/file" -import { useGlobalSync } from "@/context/global-sync" -import { useLanguage } from "@/context/language" -import { useLayout } from "@/context/layout" -import { useLocal } from "@/context/local" -import { usePermission } from "@/context/permission" -import { type ContextItem, type ImageAttachmentPart, type Prompt, usePrompt } from "@/context/prompt" -import { useSDK } from "@/context/sdk" -import { useSync } from "@/context/sync" -import { Identifier } from "@/utils/id" -import { Worktree as WorktreeState } from "@/utils/worktree" -import { buildRequestParts } from "./build-request-parts" -import { setCursorPosition } from "./editor-dom" -import { formatServerError } from "@/utils/server-errors" - -type PendingPrompt = { - abort: AbortController - cleanup: VoidFunction -} - -const pending = new Map() - -export type FollowupDraft = { - sessionID: string - sessionDirectory: string - prompt: Prompt - context: (ContextItem & { key: string })[] - agent: string - model: { providerID: string; modelID: string } - variant?: string -} - -type FollowupSendInput = { - client: ReturnType["client"] - globalSync: ReturnType - sync: ReturnType - draft: FollowupDraft - messageID?: string - optimisticBusy?: boolean - before?: () => Promise | boolean -} - -const draftText = (prompt: Prompt) => prompt.map((part) => ("content" in part ? part.content : "")).join("") - -const draftImages = (prompt: Prompt) => prompt.filter((part): part is ImageAttachmentPart => part.type === "image") - -export async function sendFollowupDraft(input: FollowupSendInput) { - const text = draftText(input.draft.prompt) - const images = draftImages(input.draft.prompt) - const [, setStore] = input.globalSync.child(input.draft.sessionDirectory) - - const setBusy = () => { - if (!input.optimisticBusy) return - setStore("session_status", input.draft.sessionID, { type: "busy" }) - } - - const setIdle = () => { - if (!input.optimisticBusy) return - setStore("session_status", input.draft.sessionID, { type: "idle" }) - } - - const wait = async () => { - const ok = await input.before?.() - if (ok === false) return false - return true - } - - const [head, ...tail] = text.split(" ") - const cmd = head?.startsWith("/") ? head.slice(1) : undefined - if (cmd && input.sync.data.command.find((item) => item.name === cmd)) { - setBusy() - try { - if (!(await wait())) { - setIdle() - return false - } - - await input.client.session.command({ - sessionID: input.draft.sessionID, - command: cmd, - arguments: tail.join(" "), - agent: input.draft.agent, - model: `${input.draft.model.providerID}/${input.draft.model.modelID}`, - variant: input.draft.variant, - parts: images.map((attachment) => ({ - id: Identifier.ascending("part"), - type: "file" as const, - mime: attachment.mime, - url: attachment.dataUrl, - filename: attachment.filename, - })), - }) - return true - } catch (err) { - setIdle() - throw err - } - } - - const messageID = input.messageID ?? Identifier.ascending("message") - const { requestParts, optimisticParts } = buildRequestParts({ - prompt: input.draft.prompt, - context: input.draft.context, - images, - text, - sessionID: input.draft.sessionID, - messageID, - sessionDirectory: input.draft.sessionDirectory, - }) - - const message: Message = { - id: messageID, - sessionID: input.draft.sessionID, - role: "user", - time: { created: Date.now() }, - agent: input.draft.agent, - model: { ...input.draft.model, variant: input.draft.variant }, - } - - const add = () => - input.sync.session.optimistic.add({ - directory: input.draft.sessionDirectory, - sessionID: input.draft.sessionID, - message, - parts: optimisticParts, - }) - - const remove = () => - input.sync.session.optimistic.remove({ - directory: input.draft.sessionDirectory, - sessionID: input.draft.sessionID, - messageID, - }) - - batch(() => { - setBusy() - add() - }) - - try { - if (!(await wait())) { - batch(() => { - setIdle() - remove() - }) - return false - } - - await input.client.session.promptAsync({ - sessionID: input.draft.sessionID, - agent: input.draft.agent, - model: input.draft.model, - messageID, - parts: requestParts, - variant: input.draft.variant, - }) - return true - } catch (err) { - batch(() => { - setIdle() - remove() - }) - throw err - } -} - -type PromptSubmitInput = { - info: Accessor<{ id: string } | undefined> - imageAttachments: Accessor - commentCount: Accessor - autoAccept: Accessor - mode: Accessor<"normal" | "shell"> - working: Accessor - editor: () => HTMLDivElement | undefined - queueScroll: () => void - promptLength: (prompt: Prompt) => number - addToHistory: (prompt: Prompt, mode: "normal" | "shell") => void - resetHistoryNavigation: () => void - setMode: (mode: "normal" | "shell") => void - setPopover: (popover: "at" | "slash" | null) => void - newSessionWorktree?: Accessor - onNewSessionWorktreeReset?: () => void - shouldQueue?: Accessor - onQueue?: (draft: FollowupDraft) => void - onAbort?: () => void - onSubmit?: () => void -} - -type CommentItem = { - path: string - selection?: FileSelection - comment?: string - commentID?: string - commentOrigin?: "review" | "file" - preview?: string -} - -export function createPromptSubmit(input: PromptSubmitInput) { - const navigate = useNavigate() - const sdk = useSDK() - const sync = useSync() - const globalSync = useGlobalSync() - const local = useLocal() - const permission = usePermission() - const prompt = usePrompt() - const layout = useLayout() - const language = useLanguage() - const params = useParams() - - const errorMessage = (err: unknown) => { - if (err && typeof err === "object" && "data" in err) { - const data = (err as { data?: { message?: string } }).data - if (data?.message) return data.message - } - if (err instanceof Error) return err.message - return language.t("common.requestFailed") - } - - const abort = async () => { - const sessionID = params.id - if (!sessionID) return Promise.resolve() - - globalSync.todo.set(sessionID, []) - const [, setStore] = globalSync.child(sdk.directory) - setStore("todo", sessionID, []) - - input.onAbort?.() - - const queued = pending.get(sessionID) - if (queued) { - queued.abort.abort() - queued.cleanup() - pending.delete(sessionID) - return Promise.resolve() - } - return sdk.client.session - .abort({ - sessionID, - }) - .catch(() => {}) - } - - const restoreCommentItems = (items: CommentItem[]) => { - for (const item of items) { - prompt.context.add({ - type: "file", - path: item.path, - selection: item.selection, - comment: item.comment, - commentID: item.commentID, - commentOrigin: item.commentOrigin, - preview: item.preview, - }) - } - } - - const removeCommentItems = (items: { key: string }[]) => { - for (const item of items) { - prompt.context.remove(item.key) - } - } - - const clearContext = () => { - for (const item of prompt.context.items()) { - prompt.context.remove(item.key) - } - } - - const seed = (dir: string, info: Session) => { - const [, setStore] = globalSync.child(dir) - setStore("session", (list: Session[]) => { - const result = Binary.search(list, info.id, (item) => item.id) - const next = [...list] - if (result.found) { - next[result.index] = info - return next - } - next.splice(result.index, 0, info) - return next - }) - } - - const handleSubmit = async (event: Event) => { - event.preventDefault() - - const currentPrompt = prompt.current() - const text = currentPrompt.map((part) => ("content" in part ? part.content : "")).join("") - const images = input.imageAttachments().slice() - const mode = input.mode() - - if (text.trim().length === 0 && images.length === 0 && input.commentCount() === 0) { - if (input.working()) void abort() - return - } - - const currentModel = local.model.current() - const currentAgent = local.agent.current() - const variant = local.model.variant.current() - if (!currentModel || !currentAgent) { - showToast({ - title: language.t("prompt.toast.modelAgentRequired.title"), - description: language.t("prompt.toast.modelAgentRequired.description"), - }) - return - } - - input.addToHistory(currentPrompt, mode) - input.resetHistoryNavigation() - - const projectDirectory = sdk.directory - const isNewSession = !params.id - const shouldAutoAccept = isNewSession && input.autoAccept() - const worktreeSelection = input.newSessionWorktree?.() || "main" - - let sessionDirectory = projectDirectory - let client = sdk.client - - if (isNewSession) { - if (worktreeSelection === "create") { - const createdWorktree = await client.worktree - .create({ directory: projectDirectory }) - .then((x) => x.data) - .catch((err) => { - showToast({ - title: language.t("prompt.toast.worktreeCreateFailed.title"), - description: errorMessage(err), - }) - return undefined - }) - - if (!createdWorktree?.directory) { - showToast({ - title: language.t("prompt.toast.worktreeCreateFailed.title"), - description: language.t("common.requestFailed"), - }) - return - } - WorktreeState.pending(createdWorktree.directory) - sessionDirectory = createdWorktree.directory - } - - if (worktreeSelection !== "main" && worktreeSelection !== "create") { - sessionDirectory = worktreeSelection - } - - if (sessionDirectory !== projectDirectory) { - client = sdk.createClient({ - directory: sessionDirectory, - throwOnError: true, - }) - globalSync.child(sessionDirectory) - } - - input.onNewSessionWorktreeReset?.() - } - - let session = input.info() - if (!session && isNewSession) { - const created = await client.session - .create() - .then((x) => x.data ?? undefined) - .catch((err) => { - showToast({ - title: language.t("prompt.toast.sessionCreateFailed.title"), - description: errorMessage(err), - }) - return undefined - }) - if (created) { - seed(sessionDirectory, created) - session = created - if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory) - local.session.promote(sessionDirectory, session.id) - layout.handoff.setTabs(base64Encode(sessionDirectory), session.id) - navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`) - } - } - if (!session) { - showToast({ - title: language.t("prompt.toast.promptSendFailed.title"), - description: language.t("prompt.toast.promptSendFailed.description"), - }) - return - } - - const model = { - modelID: currentModel.id, - providerID: currentModel.provider.id, - } - const agent = currentAgent.name - const context = prompt.context.items().slice() - const draft: FollowupDraft = { - sessionID: session.id, - sessionDirectory, - prompt: currentPrompt, - context, - agent, - model, - variant, - } - - const clearInput = () => { - prompt.reset() - input.setMode("normal") - input.setPopover(null) - } - - const restoreInput = () => { - prompt.set(currentPrompt, input.promptLength(currentPrompt)) - input.setMode(mode) - input.setPopover(null) - requestAnimationFrame(() => { - const editor = input.editor() - if (!editor) return - editor.focus() - setCursorPosition(editor, input.promptLength(currentPrompt)) - input.queueScroll() - }) - } - - if (!isNewSession && mode === "normal" && input.shouldQueue?.()) { - input.onQueue?.(draft) - clearContext() - clearInput() - return - } - - input.onSubmit?.() - - if (mode === "shell") { - clearInput() - client.session - .shell({ - sessionID: session.id, - agent, - model, - command: text, - }) - .catch((err) => { - showToast({ - title: language.t("prompt.toast.shellSendFailed.title"), - description: errorMessage(err), - }) - restoreInput() - }) - return - } - - if (text.startsWith("/")) { - const [cmdName, ...args] = text.split(" ") - const commandName = cmdName.slice(1) - const customCommand = sync.data.command.find((c) => c.name === commandName) - if (customCommand) { - clearInput() - client.session - .command({ - sessionID: session.id, - command: commandName, - arguments: args.join(" "), - agent, - model: `${model.providerID}/${model.modelID}`, - variant, - parts: images.map((attachment) => ({ - id: Identifier.ascending("part"), - type: "file" as const, - mime: attachment.mime, - url: attachment.dataUrl, - filename: attachment.filename, - })), - }) - .catch((err) => { - showToast({ - title: language.t("prompt.toast.commandSendFailed.title"), - description: formatServerError(err, language.t, language.t("common.requestFailed")), - }) - restoreInput() - }) - return - } - } - - const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim()) - const messageID = Identifier.ascending("message") - - const removeOptimisticMessage = () => { - sync.session.optimistic.remove({ - directory: sessionDirectory, - sessionID: session.id, - messageID, - }) - } - - removeCommentItems(commentItems) - clearInput() - - const waitForWorktree = async () => { - const worktree = WorktreeState.get(sessionDirectory) - if (!worktree || worktree.status !== "pending") return true - - if (sessionDirectory === projectDirectory) { - sync.set("session_status", session.id, { type: "busy" }) - } - - const controller = new AbortController() - const cleanup = () => { - if (sessionDirectory === projectDirectory) { - sync.set("session_status", session.id, { type: "idle" }) - } - removeOptimisticMessage() - restoreCommentItems(commentItems) - restoreInput() - } - - pending.set(session.id, { abort: controller, cleanup }) - - const abortWait = new Promise>>((resolve) => { - if (controller.signal.aborted) { - resolve({ status: "failed", message: "aborted" }) - return - } - controller.signal.addEventListener( - "abort", - () => { - resolve({ status: "failed", message: "aborted" }) - }, - { once: true }, - ) - }) - - const timeoutMs = 5 * 60 * 1000 - const timer = { id: undefined as number | undefined } - const timeout = new Promise>>((resolve) => { - timer.id = window.setTimeout(() => { - resolve({ - status: "failed", - message: language.t("workspace.error.stillPreparing"), - }) - }, timeoutMs) - }) - - const result = await Promise.race([WorktreeState.wait(sessionDirectory), abortWait, timeout]).finally(() => { - if (timer.id === undefined) return - clearTimeout(timer.id) - }) - pending.delete(session.id) - if (controller.signal.aborted) return false - if (result.status === "failed") throw new Error(result.message) - return true - } - - void sendFollowupDraft({ - client, - sync, - globalSync, - draft, - messageID, - optimisticBusy: sessionDirectory === projectDirectory, - before: waitForWorktree, - }).catch((err) => { - pending.delete(session.id) - if (sessionDirectory === projectDirectory) { - sync.set("session_status", session.id, { type: "idle" }) - } - showToast({ - title: language.t("prompt.toast.promptSendFailed.title"), - description: errorMessage(err), - }) - removeOptimisticMessage() - restoreCommentItems(commentItems) - restoreInput() - }) - } - - return { - abort, - handleSubmit, - } -} diff --git a/packages/app/src/components/server/server-row.tsx b/packages/app/src/components/server/server-row.tsx deleted file mode 100644 index d4f68d6306d..00000000000 --- a/packages/app/src/components/server/server-row.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { createResizeObserver } from "@solid-primitives/resize-observer" -import { - children, - createEffect, - createMemo, - createSignal, - type JSXElement, - onMount, - type ParentProps, - Show, -} from "solid-js" -import { useLanguage } from "@/context/language" -import { type ServerConnection, serverName } from "@/context/server" -import type { ServerHealth } from "@/utils/server-health" - -interface ServerRowProps extends ParentProps { - conn: ServerConnection.Any - status?: ServerHealth - class?: string - nameClass?: string - versionClass?: string - dimmed?: boolean - badge?: JSXElement - showCredentials?: boolean -} - -export function ServerRow(props: ServerRowProps) { - const language = useLanguage() - const [truncated, setTruncated] = createSignal(false) - let nameRef: HTMLSpanElement | undefined - let versionRef: HTMLSpanElement | undefined - const name = createMemo(() => serverName(props.conn)) - - const check = () => { - const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false - const versionTruncated = versionRef ? versionRef.scrollWidth > versionRef.clientWidth : false - setTruncated(nameTruncated || versionTruncated) - } - - createEffect(() => { - name() - props.conn.http.url - props.status?.version - queueMicrotask(check) - }) - - onMount(() => { - if (typeof ResizeObserver !== "function") return - createResizeObserver([nameRef, versionRef], check) - check() - }) - - const tooltipValue = () => ( - - {serverName(props.conn, true)} - - v{props.status?.version} - - - ) - - const badge = children(() => props.badge) - - return ( - -
-
-
- - {name()} - - - - v{props.status?.version} - - - } - > - {(badge) => badge()} - -
- - {(conn) => ( -
- - {conn().http.username ? ( - {conn().http.username} - ) : ( - {language.t("server.row.noUsername")} - )} - - {conn().http.password && ••••••••} -
- )} -
-
- {props.children} -
-
- ) -} - -export function ServerHealthIndicator(props: { health?: ServerHealth }) { - return ( -
- ) -} diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx deleted file mode 100644 index 6b7fe4ef7de..00000000000 --- a/packages/app/src/components/session-context-usage.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { Match, Show, Switch, createMemo } from "solid-js" -import { Tooltip, type TooltipProps } from "@opencode-ai/ui/tooltip" -import { ProgressCircle } from "@opencode-ai/ui/progress-circle" -import { Button } from "@opencode-ai/ui/button" - -import { useFile } from "@/context/file" -import { useLayout } from "@/context/layout" -import { useSync } from "@/context/sync" -import { useLanguage } from "@/context/language" -import { useProviders } from "@/hooks/use-providers" -import { getSessionContextMetrics } from "@/components/session/session-context-metrics" -import { useSessionLayout } from "@/pages/session/session-layout" -import { createSessionTabs } from "@/pages/session/helpers" - -interface SessionContextUsageProps { - variant?: "button" | "indicator" - placement?: TooltipProps["placement"] -} - -function openSessionContext(args: { - view: ReturnType["view"]> - layout: ReturnType - tabs: ReturnType["tabs"]> -}) { - if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open() - if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all") - void args.tabs.open("context") - args.tabs.setActive("context") -} - -export function SessionContextUsage(props: SessionContextUsageProps) { - const sync = useSync() - const file = useFile() - const layout = useLayout() - const language = useLanguage() - const providers = useProviders() - const { params, tabs, view } = useSessionLayout() - - const variant = createMemo(() => props.variant ?? "button") - const tabState = createSessionTabs({ - tabs, - pathFromTab: file.pathFromTab, - normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab), - }) - const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) - - const usd = createMemo( - () => - new Intl.NumberFormat(language.intl(), { - style: "currency", - currency: "USD", - }), - ) - - const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all())) - const context = createMemo(() => metrics().context) - const cost = createMemo(() => { - return usd().format(metrics().totalCost) - }) - - const openContext = () => { - if (!params.id) return - - if (tabState.activeTab() === "context") { - tabs().close("context") - return - } - openSessionContext({ - view: view(), - layout, - tabs: tabs(), - }) - } - - const circle = () => ( -
- -
- ) - - const tooltipValue = () => ( -
- - {(ctx) => ( - <> -
- {ctx().total.toLocaleString(language.intl())} - {language.t("context.usage.tokens")} -
-
- {ctx().usage ?? 0}% - {language.t("context.usage.usage")} -
- - )} -
-
- {cost()} - {language.t("context.usage.cost")} -
-
- ) - - return ( - - - - {circle()} - - - - - - - ) -} diff --git a/packages/app/src/components/session/index.ts b/packages/app/src/components/session/index.ts deleted file mode 100644 index 20124b6fdef..00000000000 --- a/packages/app/src/components/session/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { SessionHeader } from "./session-header" -export { SessionContextTab } from "./session-context-tab" -export { SortableTab, FileVisual } from "./session-sortable-tab" -export { SortableTerminalTab } from "./session-sortable-terminal-tab" -export { NewSessionView } from "./session-new-view" diff --git a/packages/app/src/components/session/session-context-breakdown.test.ts b/packages/app/src/components/session/session-context-breakdown.test.ts deleted file mode 100644 index 36c43843453..00000000000 --- a/packages/app/src/components/session/session-context-breakdown.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { Message, Part } from "@kilocode/sdk/v2/client" -import { estimateSessionContextBreakdown } from "./session-context-breakdown" - -const user = (id: string) => { - return { - id, - role: "user", - time: { created: 1 }, - } as unknown as Message -} - -const assistant = (id: string) => { - return { - id, - role: "assistant", - time: { created: 1 }, - } as unknown as Message -} - -describe("estimateSessionContextBreakdown", () => { - test("estimates tokens and keeps remaining tokens as other", () => { - const messages = [user("u1"), assistant("a1")] - const parts = { - u1: [{ type: "text", text: "hello world" }] as unknown as Part[], - a1: [{ type: "text", text: "assistant response" }] as unknown as Part[], - } - - const output = estimateSessionContextBreakdown({ - messages, - parts, - input: 20, - systemPrompt: "system prompt", - }) - - const map = Object.fromEntries(output.map((segment) => [segment.key, segment.tokens])) - expect(map.system).toBe(4) - expect(map.user).toBe(3) - expect(map.assistant).toBe(5) - expect(map.other).toBe(8) - }) - - test("scales segments when estimates exceed input", () => { - const messages = [user("u1"), assistant("a1")] - const parts = { - u1: [{ type: "text", text: "x".repeat(400) }] as unknown as Part[], - a1: [{ type: "text", text: "y".repeat(400) }] as unknown as Part[], - } - - const output = estimateSessionContextBreakdown({ - messages, - parts, - input: 10, - systemPrompt: "z".repeat(200), - }) - - const total = output.reduce((sum, segment) => sum + segment.tokens, 0) - expect(total).toBeLessThanOrEqual(10) - expect(output.every((segment) => segment.width <= 100)).toBeTrue() - }) -}) diff --git a/packages/app/src/components/session/session-context-breakdown.ts b/packages/app/src/components/session/session-context-breakdown.ts deleted file mode 100644 index 1b11fadcceb..00000000000 --- a/packages/app/src/components/session/session-context-breakdown.ts +++ /dev/null @@ -1,132 +0,0 @@ -import type { Message, Part } from "@kilocode/sdk/v2/client" - -export type SessionContextBreakdownKey = "system" | "user" | "assistant" | "tool" | "other" - -export type SessionContextBreakdownSegment = { - key: SessionContextBreakdownKey - tokens: number - width: number - percent: number -} - -const estimateTokens = (chars: number) => Math.ceil(chars / 4) -const toPercent = (tokens: number, input: number) => (tokens / input) * 100 -const toPercentLabel = (tokens: number, input: number) => Math.round(toPercent(tokens, input) * 10) / 10 - -const charsFromUserPart = (part: Part) => { - if (part.type === "text") return part.text.length - if (part.type === "file") return part.source?.text.value.length ?? 0 - if (part.type === "agent") return part.source?.value.length ?? 0 - return 0 -} - -const charsFromAssistantPart = (part: Part) => { - if (part.type === "text") return { assistant: part.text.length, tool: 0 } - if (part.type === "reasoning") return { assistant: part.text.length, tool: 0 } - if (part.type !== "tool") return { assistant: 0, tool: 0 } - - const input = Object.keys(part.state.input).length * 16 - if (part.state.status === "pending") return { assistant: 0, tool: input + part.state.raw.length } - if (part.state.status === "completed") return { assistant: 0, tool: input + part.state.output.length } - if (part.state.status === "error") return { assistant: 0, tool: input + part.state.error.length } - return { assistant: 0, tool: input } -} - -const build = ( - tokens: { system: number; user: number; assistant: number; tool: number; other: number }, - input: number, -) => { - return [ - { - key: "system", - tokens: tokens.system, - }, - { - key: "user", - tokens: tokens.user, - }, - { - key: "assistant", - tokens: tokens.assistant, - }, - { - key: "tool", - tokens: tokens.tool, - }, - { - key: "other", - tokens: tokens.other, - }, - ] - .filter((x) => x.tokens > 0) - .map((x) => ({ - key: x.key, - tokens: x.tokens, - width: toPercent(x.tokens, input), - percent: toPercentLabel(x.tokens, input), - })) as SessionContextBreakdownSegment[] -} - -export function estimateSessionContextBreakdown(args: { - messages: Message[] - parts: Record - input: number - systemPrompt?: string -}) { - if (!args.input) return [] - - const counts = args.messages.reduce( - (acc, msg) => { - const parts = args.parts[msg.id] ?? [] - if (msg.role === "user") { - const user = parts.reduce((sum, part) => sum + charsFromUserPart(part), 0) - return { ...acc, user: acc.user + user } - } - - if (msg.role !== "assistant") return acc - const assistant = parts.reduce( - (sum, part) => { - const next = charsFromAssistantPart(part) - return { - assistant: sum.assistant + next.assistant, - tool: sum.tool + next.tool, - } - }, - { assistant: 0, tool: 0 }, - ) - return { - ...acc, - assistant: acc.assistant + assistant.assistant, - tool: acc.tool + assistant.tool, - } - }, - { - system: args.systemPrompt?.length ?? 0, - user: 0, - assistant: 0, - tool: 0, - }, - ) - - const tokens = { - system: estimateTokens(counts.system), - user: estimateTokens(counts.user), - assistant: estimateTokens(counts.assistant), - tool: estimateTokens(counts.tool), - } - const estimated = tokens.system + tokens.user + tokens.assistant + tokens.tool - - if (estimated <= args.input) { - return build({ ...tokens, other: args.input - estimated }, args.input) - } - - const scale = args.input / estimated - const scaled = { - system: Math.floor(tokens.system * scale), - user: Math.floor(tokens.user * scale), - assistant: Math.floor(tokens.assistant * scale), - tool: Math.floor(tokens.tool * scale), - } - const total = scaled.system + scaled.user + scaled.assistant + scaled.tool - return build({ ...scaled, other: Math.max(0, args.input - total) }, args.input) -} diff --git a/packages/app/src/components/session/session-context-format.ts b/packages/app/src/components/session/session-context-format.ts deleted file mode 100644 index e7c536d5841..00000000000 --- a/packages/app/src/components/session/session-context-format.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { DateTime } from "luxon" - -export function createSessionContextFormatter(locale: string) { - return { - number(value: number | null | undefined) { - if (value === undefined) return "—" - if (value === null) return "—" - return value.toLocaleString(locale) - }, - percent(value: number | null | undefined) { - if (value === undefined) return "—" - if (value === null) return "—" - return value.toLocaleString(locale) + "%" - }, - time(value: number | undefined) { - if (!value) return "—" - return DateTime.fromMillis(value).setLocale(locale).toLocaleString(DateTime.DATETIME_MED) - }, - } -} diff --git a/packages/app/src/components/session/session-context-metrics.test.ts b/packages/app/src/components/session/session-context-metrics.test.ts deleted file mode 100644 index be1bbca2585..00000000000 --- a/packages/app/src/components/session/session-context-metrics.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { Message } from "@kilocode/sdk/v2/client" -import { getSessionContextMetrics } from "./session-context-metrics" - -const assistant = ( - id: string, - tokens: { input: number; output: number; reasoning: number; read: number; write: number }, - cost: number, - providerID = "openai", - modelID = "gpt-4.1", -) => { - return { - id, - role: "assistant", - providerID, - modelID, - cost, - tokens: { - input: tokens.input, - output: tokens.output, - reasoning: tokens.reasoning, - cache: { - read: tokens.read, - write: tokens.write, - }, - }, - time: { created: 1 }, - } as unknown as Message -} - -const user = (id: string) => { - return { - id, - role: "user", - cost: 0, - time: { created: 1 }, - } as unknown as Message -} - -describe("getSessionContextMetrics", () => { - test("computes totals and usage from latest assistant with tokens", () => { - const messages = [ - user("u1"), - assistant("a1", { input: 0, output: 0, reasoning: 0, read: 0, write: 0 }, 0.5), - assistant("a2", { input: 300, output: 100, reasoning: 50, read: 25, write: 25 }, 1.25), - ] - const providers = [ - { - id: "openai", - name: "OpenAI", - models: { - "gpt-4.1": { - name: "GPT-4.1", - limit: { context: 1000 }, - }, - }, - }, - ] - - const metrics = getSessionContextMetrics(messages, providers) - - expect(metrics.totalCost).toBe(1.75) - expect(metrics.context?.message.id).toBe("a2") - expect(metrics.context?.total).toBe(500) - expect(metrics.context?.usage).toBe(50) - expect(metrics.context?.providerLabel).toBe("OpenAI") - expect(metrics.context?.modelLabel).toBe("GPT-4.1") - }) - - test("preserves fallback labels and null usage when model metadata is missing", () => { - const messages = [assistant("a1", { input: 40, output: 10, reasoning: 0, read: 0, write: 0 }, 0.1, "p-1", "m-1")] - const providers = [{ id: "p-1", models: {} }] - - const metrics = getSessionContextMetrics(messages, providers) - - expect(metrics.context?.providerLabel).toBe("p-1") - expect(metrics.context?.modelLabel).toBe("m-1") - expect(metrics.context?.limit).toBeUndefined() - expect(metrics.context?.usage).toBeNull() - }) - - test("recomputes when message array is mutated in place", () => { - const messages = [assistant("a1", { input: 10, output: 10, reasoning: 10, read: 10, write: 10 }, 0.25)] - const providers = [{ id: "openai", models: {} }] - - const one = getSessionContextMetrics(messages, providers) - messages.push(assistant("a2", { input: 100, output: 20, reasoning: 0, read: 0, write: 0 }, 0.75)) - const two = getSessionContextMetrics(messages, providers) - - expect(one.context?.message.id).toBe("a1") - expect(two.context?.message.id).toBe("a2") - expect(two.totalCost).toBe(1) - }) - - test("returns empty metrics when inputs are undefined", () => { - const metrics = getSessionContextMetrics(undefined, undefined) - - expect(metrics.totalCost).toBe(0) - expect(metrics.context).toBeUndefined() - }) -}) diff --git a/packages/app/src/components/session/session-context-metrics.ts b/packages/app/src/components/session/session-context-metrics.ts deleted file mode 100644 index 4bdcff52ebb..00000000000 --- a/packages/app/src/components/session/session-context-metrics.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { AssistantMessage, Message } from "@kilocode/sdk/v2/client" - -type Provider = { - id: string - name?: string - models: Record -} - -type Model = { - name?: string - limit: { - context: number - } -} - -type Context = { - message: AssistantMessage - provider?: Provider - model?: Model - providerLabel: string - modelLabel: string - limit: number | undefined - input: number - output: number - reasoning: number - cacheRead: number - cacheWrite: number - total: number - usage: number | null -} - -type Metrics = { - totalCost: number - context: Context | undefined -} - -const tokenTotal = (msg: AssistantMessage) => { - return msg.tokens.input + msg.tokens.output + msg.tokens.reasoning + msg.tokens.cache.read + msg.tokens.cache.write -} - -const lastAssistantWithTokens = (messages: Message[]) => { - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i] - if (msg.role !== "assistant") continue - if (tokenTotal(msg) <= 0) continue - return msg - } -} - -const build = (messages: Message[] = [], providers: Provider[] = []): Metrics => { - const totalCost = messages.reduce((sum, msg) => sum + (msg.role === "assistant" ? msg.cost : 0), 0) - const message = lastAssistantWithTokens(messages) - if (!message) return { totalCost, context: undefined } - - const provider = providers.find((item) => item.id === message.providerID) - const model = provider?.models[message.modelID] - const limit = model?.limit.context - const total = tokenTotal(message) - - return { - totalCost, - context: { - message, - provider, - model, - providerLabel: provider?.name ?? message.providerID, - modelLabel: model?.name ?? message.modelID, - limit, - input: message.tokens.input, - output: message.tokens.output, - reasoning: message.tokens.reasoning, - cacheRead: message.tokens.cache.read, - cacheWrite: message.tokens.cache.write, - total, - usage: limit ? Math.round((total / limit) * 100) : null, - }, - } -} - -export function getSessionContextMetrics(messages: Message[] = [], providers: Provider[] = []) { - return build(messages, providers) -} diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx deleted file mode 100644 index 2fa44f1c7e1..00000000000 --- a/packages/app/src/components/session/session-context-tab.tsx +++ /dev/null @@ -1,341 +0,0 @@ -import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js" -import type { JSX } from "solid-js" -import { useSync } from "@/context/sync" -import { checksum } from "@opencode-ai/shared/util/encode" -import { findLast } from "@opencode-ai/shared/util/array" -import { same } from "@/utils/same" -import { Icon } from "@opencode-ai/ui/icon" -import { Accordion } from "@opencode-ai/ui/accordion" -import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header" -import { File } from "@opencode-ai/ui/file" -import { Markdown } from "@opencode-ai/ui/markdown" -import { ScrollView } from "@opencode-ai/ui/scroll-view" -import type { Message, Part, UserMessage } from "@kilocode/sdk/v2/client" -import { useLanguage } from "@/context/language" -import { useProviders } from "@/hooks/use-providers" -import { useSessionLayout } from "@/pages/session/session-layout" -import { getSessionContextMetrics } from "./session-context-metrics" -import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown" -import { createSessionContextFormatter } from "./session-context-format" - -const BREAKDOWN_COLOR: Record = { - system: "var(--syntax-info)", - user: "var(--syntax-success)", - assistant: "var(--syntax-property)", - tool: "var(--syntax-warning)", - other: "var(--syntax-comment)", -} - -function Stat(props: { label: string; value: JSX.Element }) { - return ( -
-
{props.label}
-
{props.value}
-
- ) -} - -function RawMessageContent(props: { message: Message; getParts: (id: string) => Part[]; onRendered: () => void }) { - const file = createMemo(() => { - const parts = props.getParts(props.message.id) - const contents = JSON.stringify({ message: props.message, parts }, null, 2) - return { - name: `${props.message.role}-${props.message.id}.json`, - contents, - cacheKey: checksum(contents), - } - }) - - return ( - requestAnimationFrame(props.onRendered)} - /> - ) -} - -function RawMessage(props: { - message: Message - getParts: (id: string) => Part[] - onRendered: () => void - time: (value: number | undefined) => string -}) { - return ( - - - -
-
- {props.message.role} • {props.message.id} -
-
-
{props.time(props.message.time.created)}
- -
-
-
-
- -
- -
-
-
- ) -} - -const emptyMessages: Message[] = [] -const emptyUserMessages: UserMessage[] = [] - -export function SessionContextTab() { - const sync = useSync() - const language = useLanguage() - const providers = useProviders() - const { params, view } = useSessionLayout() - - const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) - - const messages = createMemo( - () => { - const id = params.id - if (!id) return emptyMessages - return (sync.data.message[id] ?? []) as Message[] - }, - emptyMessages, - { equals: same }, - ) - - const userMessages = createMemo( - () => messages().filter((m) => m.role === "user") as UserMessage[], - emptyUserMessages, - { equals: same }, - ) - - const visibleUserMessages = createMemo( - () => { - const revert = info()?.revert?.messageID - if (!revert) return userMessages() - return userMessages().filter((m) => m.id < revert) - }, - emptyUserMessages, - { equals: same }, - ) - - const usd = createMemo( - () => - new Intl.NumberFormat(language.intl(), { - style: "currency", - currency: "USD", - }), - ) - - const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all())) - const ctx = createMemo(() => metrics().context) - const formatter = createMemo(() => createSessionContextFormatter(language.intl())) - - const cost = createMemo(() => { - return usd().format(metrics().totalCost) - }) - - const counts = createMemo(() => { - const all = messages() - const user = all.reduce((count, x) => count + (x.role === "user" ? 1 : 0), 0) - const assistant = all.reduce((count, x) => count + (x.role === "assistant" ? 1 : 0), 0) - return { - all: all.length, - user, - assistant, - } - }) - - const systemPrompt = createMemo(() => { - const msg = findLast(visibleUserMessages(), (m) => !!m.system) - const system = msg?.system - if (!system) return - const trimmed = system.trim() - if (!trimmed) return - return trimmed - }) - - const providerLabel = createMemo(() => { - const c = ctx() - if (!c) return "—" - return c.providerLabel - }) - - const modelLabel = createMemo(() => { - const c = ctx() - if (!c) return "—" - return c.modelLabel - }) - - const breakdown = createMemo( - on( - () => [ctx()?.message.id, ctx()?.input, messages().length, systemPrompt()], - () => { - const c = ctx() - if (!c?.input) return [] - return estimateSessionContextBreakdown({ - messages: messages(), - parts: sync.data.part as Record, - input: c.input, - systemPrompt: systemPrompt(), - }) - }, - ), - ) - - const breakdownLabel = (key: SessionContextBreakdownKey) => { - if (key === "system") return language.t("context.breakdown.system") - if (key === "user") return language.t("context.breakdown.user") - if (key === "assistant") return language.t("context.breakdown.assistant") - if (key === "tool") return language.t("context.breakdown.tool") - return language.t("context.breakdown.other") - } - - const stats = [ - { label: "context.stats.session", value: () => info()?.title ?? params.id ?? "—" }, - { label: "context.stats.messages", value: () => counts().all.toLocaleString(language.intl()) }, - { label: "context.stats.provider", value: providerLabel }, - { label: "context.stats.model", value: modelLabel }, - { label: "context.stats.limit", value: () => formatter().number(ctx()?.limit) }, - { label: "context.stats.totalTokens", value: () => formatter().number(ctx()?.total) }, - { label: "context.stats.usage", value: () => formatter().percent(ctx()?.usage) }, - { label: "context.stats.inputTokens", value: () => formatter().number(ctx()?.input) }, - { label: "context.stats.outputTokens", value: () => formatter().number(ctx()?.output) }, - { label: "context.stats.reasoningTokens", value: () => formatter().number(ctx()?.reasoning) }, - { - label: "context.stats.cacheTokens", - value: () => `${formatter().number(ctx()?.cacheRead)} / ${formatter().number(ctx()?.cacheWrite)}`, - }, - { label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.intl()) }, - { label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.intl()) }, - { label: "context.stats.totalCost", value: cost }, - { label: "context.stats.sessionCreated", value: () => formatter().time(info()?.time.created) }, - { label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) }, - ] satisfies { label: string; value: () => JSX.Element }[] - - let scroll: HTMLDivElement | undefined - let frame: number | undefined - let pending: { x: number; y: number } | undefined - const getParts = (id: string) => (sync.data.part[id] ?? []) as Part[] - - const restoreScroll = () => { - const el = scroll - if (!el) return - - const s = view().scroll("context") - if (!s) return - - if (el.scrollTop !== s.y) el.scrollTop = s.y - if (el.scrollLeft !== s.x) el.scrollLeft = s.x - } - - const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => { - pending = { - x: event.currentTarget.scrollLeft, - y: event.currentTarget.scrollTop, - } - if (frame !== undefined) return - - frame = requestAnimationFrame(() => { - frame = undefined - - const next = pending - pending = undefined - if (!next) return - - view().setScroll("context", next) - }) - } - - createEffect( - on( - () => messages().length, - () => { - requestAnimationFrame(restoreScroll) - }, - { defer: true }, - ), - ) - - onCleanup(() => { - if (frame === undefined) return - cancelAnimationFrame(frame) - }) - - return ( - { - scroll = el - restoreScroll() - }} - onScroll={handleScroll} - > -
-
- - {(stat) => [0])} value={stat.value()} />} - -
- - 0}> -
-
{language.t("context.breakdown.title")}
-
- - {(segment) => ( -
- )} - -
-
- - {(segment) => ( -
-
-
{breakdownLabel(segment.key)}
-
{segment.percent.toLocaleString(language.intl())}%
-
- )} - -
- -
- - - - {(prompt) => ( -
-
{language.t("context.systemPrompt.title")}
-
- -
-
- )} -
- -
-
{language.t("context.rawMessages.title")}
- - - {(message) => ( - - )} - - -
-
- - ) -} diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx deleted file mode 100644 index d61335d9f0d..00000000000 --- a/packages/app/src/components/session/session-header.tsx +++ /dev/null @@ -1,503 +0,0 @@ -import { AppIcon } from "@opencode-ai/ui/app-icon" -import { Button } from "@opencode-ai/ui/button" -import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" -import { Icon } from "@opencode-ai/ui/icon" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Keybind } from "@opencode-ai/ui/keybind" -import { Spinner } from "@opencode-ai/ui/spinner" -import { showToast } from "@opencode-ai/ui/toast" -import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { getFilename } from "@opencode-ai/shared/util/path" -import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js" -import { createStore } from "solid-js/store" -import { Portal } from "solid-js/web" -import { useCommand } from "@/context/command" -import { useLanguage } from "@/context/language" -import { useLayout } from "@/context/layout" -import { usePlatform } from "@/context/platform" -import { useServer } from "@/context/server" -import { useSettings } from "@/context/settings" -import { useSync } from "@/context/sync" -import { useTerminal } from "@/context/terminal" -import { focusTerminalById } from "@/pages/session/helpers" -import { useSessionLayout } from "@/pages/session/session-layout" -import { messageAgentColor } from "@/utils/agent" -import { decode64 } from "@/utils/base64" -import { Persist, persisted } from "@/utils/persist" -import { StatusPopover } from "../status-popover" - -const OPEN_APPS = [ - "vscode", - "cursor", - "zed", - "textmate", - "antigravity", - "finder", - "terminal", - "iterm2", - "ghostty", - "warp", - "xcode", - "android-studio", - "powershell", - "sublime-text", -] as const - -type OpenApp = (typeof OPEN_APPS)[number] -type OS = "macos" | "windows" | "linux" | "unknown" - -const MAC_APPS = [ - { - id: "vscode", - label: "session.header.open.app.vscode", - icon: "vscode", - openWith: "Visual Studio Code", - }, - { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "Cursor" }, - { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "Zed" }, - { id: "textmate", label: "session.header.open.app.textmate", icon: "textmate", openWith: "TextMate" }, - { - id: "antigravity", - label: "session.header.open.app.antigravity", - icon: "antigravity", - openWith: "Antigravity", - }, - { id: "terminal", label: "session.header.open.app.terminal", icon: "terminal", openWith: "Terminal" }, - { id: "iterm2", label: "session.header.open.app.iterm2", icon: "iterm2", openWith: "iTerm" }, - { id: "ghostty", label: "session.header.open.app.ghostty", icon: "ghostty", openWith: "Ghostty" }, - { id: "warp", label: "session.header.open.app.warp", icon: "warp", openWith: "Warp" }, - { id: "xcode", label: "session.header.open.app.xcode", icon: "xcode", openWith: "Xcode" }, - { - id: "android-studio", - label: "session.header.open.app.androidStudio", - icon: "android-studio", - openWith: "Android Studio", - }, - { - id: "sublime-text", - label: "session.header.open.app.sublimeText", - icon: "sublime-text", - openWith: "Sublime Text", - }, -] as const - -const WINDOWS_APPS = [ - { id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" }, - { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" }, - { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" }, - { - id: "powershell", - label: "session.header.open.app.powershell", - icon: "powershell", - openWith: "powershell", - }, - { - id: "sublime-text", - label: "session.header.open.app.sublimeText", - icon: "sublime-text", - openWith: "Sublime Text", - }, -] as const - -const LINUX_APPS = [ - { id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" }, - { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" }, - { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" }, - { - id: "sublime-text", - label: "session.header.open.app.sublimeText", - icon: "sublime-text", - openWith: "Sublime Text", - }, -] as const - -const detectOS = (platform: ReturnType): OS => { - if (platform.platform === "desktop" && platform.os) return platform.os - if (typeof navigator !== "object") return "unknown" - const value = navigator.platform || navigator.userAgent - if (/Mac/i.test(value)) return "macos" - if (/Win/i.test(value)) return "windows" - if (/Linux/i.test(value)) return "linux" - return "unknown" -} - -const showRequestError = (language: ReturnType, err: unknown) => { - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: err instanceof Error ? err.message : String(err), - }) -} - -export function SessionHeader() { - const layout = useLayout() - const command = useCommand() - const server = useServer() - const platform = usePlatform() - const language = useLanguage() - const settings = useSettings() - const sync = useSync() - const terminal = useTerminal() - const { params, view } = useSessionLayout() - - const projectDirectory = createMemo(() => decode64(params.dir) ?? "") - const project = createMemo(() => { - const directory = projectDirectory() - if (!directory) return - return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory)) - }) - const name = createMemo(() => { - const current = project() - if (current) return current.name || getFilename(current.worktree) - return getFilename(projectDirectory()) - }) - const hotkey = createMemo(() => command.keybind("file.open")) - const os = createMemo(() => detectOS(platform)) - const isDesktopBeta = platform.platform === "desktop" && import.meta.env.VITE_KILO_CHANNEL === "beta" - const search = createMemo(() => !isDesktopBeta || settings.general.showSearch()) - const tree = createMemo(() => !isDesktopBeta || settings.general.showFileTree()) - const term = createMemo(() => !isDesktopBeta || settings.general.showTerminal()) - const status = createMemo(() => !isDesktopBeta || settings.general.showStatus()) - - const [exists, setExists] = createStore>>({ - finder: true, - }) - - const apps = createMemo(() => { - if (os() === "macos") return MAC_APPS - if (os() === "windows") return WINDOWS_APPS - return LINUX_APPS - }) - - const fileManager = createMemo(() => { - if (os() === "macos") return { label: "session.header.open.finder", icon: "finder" as const } - if (os() === "windows") return { label: "session.header.open.fileExplorer", icon: "file-explorer" as const } - return { label: "session.header.open.fileManager", icon: "finder" as const } - }) - - createEffect(() => { - if (platform.platform !== "desktop") return - if (!platform.checkAppExists) return - - const list = apps() - - setExists(Object.fromEntries(list.map((app) => [app.id, undefined])) as Partial>) - - void Promise.all( - list.map((app) => - Promise.resolve(platform.checkAppExists?.(app.openWith)) - .then((value) => Boolean(value)) - .catch(() => false) - .then((ok) => [app.id, ok] as const), - ), - ).then((entries) => { - setExists(Object.fromEntries(entries) as Partial>) - }) - }) - - const options = createMemo(() => { - return [ - { id: "finder", label: language.t(fileManager().label), icon: fileManager().icon }, - ...apps() - .filter((app) => exists[app.id]) - .map((app) => ({ ...app, label: language.t(app.label) })), - ] as const - }) - - const toggleTerminal = () => { - const next = !view().terminal.opened() - view().terminal.toggle() - if (!next) return - - const id = terminal.active() - if (!id) return - focusTerminalById(id) - } - - const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp })) - const [menu, setMenu] = createStore({ open: false }) - const [openRequest, setOpenRequest] = createStore({ - app: undefined as OpenApp | undefined, - }) - - const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal()) - const current = createMemo( - () => - options().find((o) => o.id === prefs.app) ?? - options()[0] ?? - ({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const), - ) - const opening = createMemo(() => openRequest.app !== undefined) - const tint = createMemo(() => - messageAgentColor(params.id ? sync.data.message[params.id] : undefined, sync.data.agent), - ) - - const selectApp = (app: OpenApp) => { - if (!options().some((item) => item.id === app)) return - setPrefs("app", app) - } - - const openDir = (app: OpenApp) => { - if (opening() || !canOpen() || !platform.openPath) return - const directory = projectDirectory() - if (!directory) return - - const item = options().find((o) => o.id === app) - const openWith = item && "openWith" in item ? item.openWith : undefined - setOpenRequest("app", app) - platform - .openPath(directory, openWith) - .catch((err: unknown) => showRequestError(language, err)) - .finally(() => { - setOpenRequest("app", undefined) - }) - } - - const copyPath = () => { - const directory = projectDirectory() - if (!directory) return - navigator.clipboard - .writeText(directory) - .then(() => { - showToast({ - variant: "success", - icon: "circle-check", - title: language.t("session.share.copy.copied"), - description: directory, - }) - }) - .catch((err: unknown) => showRequestError(language, err)) - } - - const [centerMount, setCenterMount] = createSignal(null) - const [rightMount, setRightMount] = createSignal(null) - onMount(() => { - setCenterMount(document.getElementById("opencode-titlebar-center")) - setRightMount(document.getElementById("opencode-titlebar-right")) - }) - - return ( - <> - - {(mount) => ( - - - - )} - - - {(mount) => ( - -
- - - } - > -
-
- - setMenu("open", open)} - > - - - - - - {language.t("session.header.openIn")} - - { - if (!OPEN_APPS.includes(value as OpenApp)) return - selectApp(value as OpenApp) - }} - > - - {(o) => ( - { - setMenu("open", false) - openDir(o.id) - }} - > -
- -
- {o.label} - - - -
- )} -
-
-
- - { - setMenu("open", false) - copyPath() - }} - > -
- -
- - {language.t("session.header.open.copyPath")} - -
-
-
-
-
-
-
-
-
-
- - - - - - - - - - - - -
-
- - )} -
- - ) -} diff --git a/packages/app/src/components/session/session-new-view.tsx b/packages/app/src/components/session/session-new-view.tsx deleted file mode 100644 index d2cac28fc43..00000000000 --- a/packages/app/src/components/session/session-new-view.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { Show, createMemo } from "solid-js" -import { DateTime } from "luxon" -import { useSync } from "@/context/sync" -import { useSDK } from "@/context/sdk" -import { useLanguage } from "@/context/language" -import { Icon } from "@opencode-ai/ui/icon" -import { Mark } from "@opencode-ai/ui/logo" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" - -const MAIN_WORKTREE = "main" -const CREATE_WORKTREE = "create" -const ROOT_CLASS = "size-full flex flex-col" - -interface NewSessionViewProps { - worktree: string -} - -export function NewSessionView(props: NewSessionViewProps) { - const sync = useSync() - const sdk = useSDK() - const language = useLanguage() - - const sandboxes = createMemo(() => sync.project?.sandboxes ?? []) - const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE]) - const current = createMemo(() => { - const selection = props.worktree - if (options().includes(selection)) return selection - return MAIN_WORKTREE - }) - const projectRoot = createMemo(() => sync.project?.worktree ?? sdk.directory) - const isWorktree = createMemo(() => { - const project = sync.project - if (!project) return false - return sdk.directory !== project.worktree - }) - - const label = (value: string) => { - if (value === MAIN_WORKTREE) { - if (isWorktree()) return language.t("session.new.worktree.main") - const branch = sync.data.vcs?.branch - if (branch) return language.t("session.new.worktree.mainWithBranch", { branch }) - return language.t("session.new.worktree.main") - } - - if (value === CREATE_WORKTREE) return language.t("session.new.worktree.create") - - return getFilename(value) - } - - return ( -
-
-
-
-
- -
{language.t("session.new.title")}
-
-
-
-
- {getDirectory(projectRoot())} - {getFilename(projectRoot())} -
-
-
- -
- {label(current())} -
-
- - {(project) => ( -
-
- {language.t("session.new.lastModified")}  - - {DateTime.fromMillis(project().time.updated ?? project().time.created) - .setLocale(language.intl()) - .toRelative()} - -
-
- )} -
-
-
-
-
- ) -} diff --git a/packages/app/src/components/session/session-sortable-tab.tsx b/packages/app/src/components/session/session-sortable-tab.tsx deleted file mode 100644 index fb2275c445a..00000000000 --- a/packages/app/src/components/session/session-sortable-tab.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { createMemo, Show } from "solid-js" -import type { JSX } from "solid-js" -import { createSortable } from "@thisbeyond/solid-dnd" -import { FileIcon } from "@opencode-ai/ui/file-icon" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { Tabs } from "@opencode-ai/ui/tabs" -import { getFilename } from "@opencode-ai/shared/util/path" -import { useFile } from "@/context/file" -import { useLanguage } from "@/context/language" -import { useCommand } from "@/context/command" - -export function FileVisual(props: { path: string; active?: boolean }): JSX.Element { - return ( -
- } - > - - - - - - {getFilename(props.path)} -
- ) -} - -export function SortableTab(props: { tab: string; onTabClose: (tab: string) => void }): JSX.Element { - const file = useFile() - const language = useLanguage() - const command = useCommand() - const sortable = createSortable(props.tab) - const path = createMemo(() => file.pathFromTab(props.tab)) - const content = createMemo(() => { - const value = path() - if (!value) return - return - }) - return ( -
-
- - props.onTabClose(props.tab)} - aria-label={language.t("common.closeTab")} - /> - - } - hideCloseButton - onMiddleClick={() => props.onTabClose(props.tab)} - > - {(value) => value()} - -
-
- ) -} diff --git a/packages/app/src/components/session/session-sortable-terminal-tab.tsx b/packages/app/src/components/session/session-sortable-terminal-tab.tsx deleted file mode 100644 index 2d88ed18064..00000000000 --- a/packages/app/src/components/session/session-sortable-terminal-tab.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import type { JSX } from "solid-js" -import { Show, createEffect, onCleanup } from "solid-js" -import { createStore } from "solid-js/store" -import { createSortable } from "@thisbeyond/solid-dnd" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Tabs } from "@opencode-ai/ui/tabs" -import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" -import { Icon } from "@opencode-ai/ui/icon" -import { isDefaultTitle as isDefaultTerminalTitle } from "@/context/terminal-title" -import { useTerminal, type LocalPTY } from "@/context/terminal" -import { useLanguage } from "@/context/language" -import { focusTerminalById } from "@/pages/session/helpers" - -export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => void }): JSX.Element { - const terminal = useTerminal() - const language = useLanguage() - const sortable = createSortable(props.terminal.id) - const [store, setStore] = createStore({ - editing: false, - title: props.terminal.title, - menuOpen: false, - menuPosition: { x: 0, y: 0 }, - blurEnabled: false, - }) - let input: HTMLInputElement | undefined - let blurFrame: number | undefined - let editRequested = false - - const isDefaultTitle = () => { - const number = props.terminal.titleNumber - if (!Number.isFinite(number) || number <= 0) return false - return isDefaultTerminalTitle(props.terminal.title, number) - } - - const label = () => { - language.locale() - if (props.terminal.title && !isDefaultTitle()) return props.terminal.title - - const number = props.terminal.titleNumber - if (Number.isFinite(number) && number > 0) return language.t("terminal.title.numbered", { number }) - if (props.terminal.title) return props.terminal.title - return language.t("terminal.title") - } - - const close = () => { - const count = terminal.all().length - void terminal.close(props.terminal.id) - if (count === 1) { - props.onClose?.() - } - } - - const focus = () => { - if (store.editing) return - if (document.activeElement instanceof HTMLElement) document.activeElement.blur() - focusTerminalById(props.terminal.id) - } - - const edit = (e?: Event) => { - if (e) { - e.stopPropagation() - e.preventDefault() - } - - setStore("blurEnabled", false) - setStore("title", props.terminal.title) - setStore("editing", true) - } - - const save = () => { - if (!store.blurEnabled) return - - const value = store.title.trim() - if (value && value !== props.terminal.title) { - terminal.update({ id: props.terminal.id, title: value }) - } - setStore("editing", false) - } - - const keydown = (e: KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault() - save() - return - } - if (e.key === "Escape") { - e.preventDefault() - setStore("editing", false) - } - } - - const menu = (e: MouseEvent) => { - e.preventDefault() - setStore("menuPosition", { x: e.clientX, y: e.clientY }) - setStore("menuOpen", true) - } - - createEffect(() => { - if (!store.editing) return - if (!input) return - input.focus() - input.select() - if (blurFrame !== undefined) cancelAnimationFrame(blurFrame) - blurFrame = requestAnimationFrame(() => { - blurFrame = undefined - setStore("blurEnabled", true) - }) - }) - - onCleanup(() => { - if (blurFrame === undefined) return - cancelAnimationFrame(blurFrame) - }) - - return ( -
-
- e.preventDefault()} - onContextMenu={menu} - class="!shadow-none" - classes={{ - button: "border-0 outline-none focus:outline-none focus-visible:outline-none !shadow-none !ring-0", - }} - closeButton={ - { - e.stopPropagation() - close() - }} - aria-label={language.t("terminal.close")} - /> - } - > - - {label()} - - - -
- setStore("title", e.currentTarget.value)} - onBlur={save} - onKeyDown={keydown} - onMouseDown={(e) => e.stopPropagation()} - class="bg-transparent border-none outline-none text-sm min-w-0 flex-1" - /> -
-
- setStore("menuOpen", open)}> - - { - if (!editRequested) return - e.preventDefault() - editRequested = false - requestAnimationFrame(() => edit()) - }} - > - (editRequested = true)}> - - {language.t("common.rename")} - - - - {language.t("common.close")} - - - - -
-
- ) -} diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx deleted file mode 100644 index 50c511d0dd9..00000000000 --- a/packages/app/src/components/settings-general.tsx +++ /dev/null @@ -1,744 +0,0 @@ -import { Component, Show, createMemo, createResource, onMount, type JSX } from "solid-js" -import { createStore } from "solid-js/store" -import { Button } from "@opencode-ai/ui/button" -import { Icon } from "@opencode-ai/ui/icon" -import { Select } from "@opencode-ai/ui/select" -import { Switch } from "@opencode-ai/ui/switch" -import { TextField } from "@opencode-ai/ui/text-field" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context" -import { showToast } from "@opencode-ai/ui/toast" -import { useParams } from "@solidjs/router" -import { useLanguage } from "@/context/language" -import { usePermission } from "@/context/permission" -import { usePlatform } from "@/context/platform" -import { - monoDefault, - monoFontFamily, - monoInput, - sansDefault, - sansFontFamily, - sansInput, - terminalDefault, - terminalFontFamily, - terminalInput, - useSettings, -} from "@/context/settings" -import { decode64 } from "@/utils/base64" -import { playSoundById, SOUND_OPTIONS } from "@/utils/sound" -import { Link } from "./link" -import { SettingsList } from "./settings-list" - -let demoSoundState = { - cleanup: undefined as (() => void) | undefined, - timeout: undefined as NodeJS.Timeout | undefined, - run: 0, -} - -type ThemeOption = { - id: string - name: string -} - -// To prevent audio from overlapping/playing very quickly when navigating the settings menus, -// delay the playback by 100ms during quick selection changes and pause existing sounds. -const stopDemoSound = () => { - demoSoundState.run += 1 - if (demoSoundState.cleanup) { - demoSoundState.cleanup() - } - clearTimeout(demoSoundState.timeout) - demoSoundState.cleanup = undefined -} - -const playDemoSound = (id: string | undefined) => { - stopDemoSound() - if (!id) return - - const run = ++demoSoundState.run - demoSoundState.timeout = setTimeout(() => { - void playSoundById(id).then((cleanup) => { - if (demoSoundState.run !== run) { - cleanup?.() - return - } - demoSoundState.cleanup = cleanup - }) - }, 100) -} - -export const SettingsGeneral: Component = () => { - const theme = useTheme() - const language = useLanguage() - const permission = usePermission() - const platform = usePlatform() - const params = useParams() - const settings = useSettings() - - onMount(() => { - void theme.loadThemes() - }) - - const [store, setStore] = createStore({ - checking: false, - }) - - const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux") - const dir = createMemo(() => decode64(params.dir)) - const accepting = createMemo(() => { - const value = dir() - if (!value) return false - if (!params.id) return permission.isAutoAcceptingDirectory(value) - return permission.isAutoAccepting(params.id, value) - }) - - const toggleAccept = (checked: boolean) => { - const value = dir() - if (!value) return - - if (!params.id) { - if (permission.isAutoAcceptingDirectory(value) === checked) return - permission.toggleAutoAcceptDirectory(value) - return - } - - if (checked) { - permission.enableAutoAccept(params.id, value) - return - } - - permission.disableAutoAccept(params.id, value) - } - const desktop = createMemo(() => platform.platform === "desktop") - - const check = () => { - if (!platform.checkUpdate) return - setStore("checking", true) - - void platform - .checkUpdate() - .then((result) => { - if (!result.updateAvailable) { - showToast({ - variant: "success", - icon: "circle-check", - title: language.t("settings.updates.toast.latest.title"), - description: language.t("settings.updates.toast.latest.description", { version: platform.version ?? "" }), - }) - return - } - - const actions = platform.updateAndRestart - ? [ - { - label: language.t("toast.update.action.installRestart"), - onClick: async () => { - await platform.updateAndRestart!() - }, - }, - { - label: language.t("toast.update.action.notYet"), - onClick: "dismiss" as const, - }, - ] - : [ - { - label: language.t("toast.update.action.notYet"), - onClick: "dismiss" as const, - }, - ] - - showToast({ - persistent: true, - icon: "download", - title: language.t("toast.update.title"), - description: language.t("toast.update.description", { version: result.version ?? "" }), - actions, - }) - }) - .catch((err: unknown) => { - const message = err instanceof Error ? err.message : String(err) - showToast({ title: language.t("common.requestFailed"), description: message }) - }) - .finally(() => setStore("checking", false)) - } - - const themeOptions = createMemo(() => theme.ids().map((id) => ({ id, name: theme.name(id) }))) - - const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [ - { value: "system", label: language.t("theme.scheme.system") }, - { value: "light", label: language.t("theme.scheme.light") }, - { value: "dark", label: language.t("theme.scheme.dark") }, - ]) - - const languageOptions = createMemo(() => - language.locales.map((locale) => ({ - value: locale, - label: language.label(locale), - })), - ) - - const noneSound = { id: "none", label: "sound.option.none" } as const - const soundOptions = [noneSound, ...SOUND_OPTIONS] - const mono = () => monoInput(settings.appearance.font()) - const sans = () => sansInput(settings.appearance.uiFont()) - const terminal = () => terminalInput(settings.appearance.terminalFont()) - - const soundSelectProps = ( - enabled: () => boolean, - current: () => string, - setEnabled: (value: boolean) => void, - set: (id: string) => void, - ) => ({ - options: soundOptions, - current: enabled() ? (soundOptions.find((o) => o.id === current()) ?? noneSound) : noneSound, - value: (o: (typeof soundOptions)[number]) => o.id, - label: (o: (typeof soundOptions)[number]) => language.t(o.label), - onHighlight: (option: (typeof soundOptions)[number] | undefined) => { - if (!option) return - playDemoSound(option.id === "none" ? undefined : option.id) - }, - onSelect: (option: (typeof soundOptions)[number] | undefined) => { - if (!option) return - if (option.id === "none") { - setEnabled(false) - stopDemoSound() - return - } - setEnabled(true) - set(option.id) - playDemoSound(option.id) - }, - variant: "secondary" as const, - size: "small" as const, - triggerVariant: "settings" as const, - }) - - const GeneralSection = () => ( -
- - - o.value === theme.colorScheme())} - value={(o) => o.value} - label={(o) => o.label} - onSelect={(option) => option && theme.setColorScheme(option.value)} - onHighlight={(option) => { - if (!option) return - theme.previewColorScheme(option.value) - return () => theme.cancelPreview() - }} - variant="secondary" - size="small" - triggerVariant="settings" - triggerStyle={{ "min-width": "220px" }} - /> - - - - {language.t("settings.general.row.theme.description")}{" "} - {language.t("common.learnMore")} - - } - > - settings.sounds.agentEnabled(), - () => settings.sounds.agent(), - (value) => settings.sounds.setAgentEnabled(value), - (id) => settings.sounds.setAgent(id), - )} - /> - - - - settings.sounds.errorsEnabled(), - () => settings.sounds.errors(), - (value) => settings.sounds.setErrorsEnabled(value), - (id) => settings.sounds.setErrors(id), - )} - /> - - -
- ) - - const UpdatesSection = () => ( -
-

{language.t("settings.general.section.updates")}

- - - -
- settings.updates.setStartup(checked)} - /> -
-
- - -
- settings.general.setReleaseNotes(checked)} - /> -
-
- - - - -
-
- ) - - console.log(import.meta.env) - return ( -
-
-
-

{language.t("settings.tab.general")}

-
-
- -
- - - - - - - - - {/* - {(_) => { - const [enabledResource, actions] = createResource(() => platform.getWslEnabled?.()) - const enabled = () => (enabledResource.state === "pending" ? undefined : enabledResource.latest) - - return ( -
-

{language.t("settings.desktop.section.wsl")}

- - - -
- platform.setWslEnabled?.(checked)?.finally(() => actions.refetch())} - /> -
-
-
-
- ) - }} -
*/} - - - - - {(_) => { - const [valueResource, actions] = createResource(() => platform.getDisplayBackend?.()) - const value = () => (valueResource.state === "pending" ? undefined : valueResource.latest) - - const onChange = (checked: boolean) => - platform.setDisplayBackend?.(checked ? "wayland" : "auto").finally(() => actions.refetch()) - - return ( -
-

{language.t("settings.general.section.display")}

- - - - {language.t("settings.general.row.wayland.title")} - - - - - -
- } - description={language.t("settings.general.row.wayland.description")} - > -
- -
- - -
- ) - }} - - - - - -
-
- ) -} - -interface SettingsRowProps { - title: string | JSX.Element - description: string | JSX.Element - children: JSX.Element -} - -const SettingsRow: Component = (props) => { - return ( -
-
- {props.title} - {props.description} -
-
{props.children}
-
- ) -} diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx deleted file mode 100644 index 7d2dfaa6363..00000000000 --- a/packages/app/src/components/settings-keybinds.tsx +++ /dev/null @@ -1,453 +0,0 @@ -import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js" -import { createStore } from "solid-js/store" -import { makeEventListener } from "@solid-primitives/event-listener" -import { Button } from "@opencode-ai/ui/button" -import { Icon } from "@opencode-ai/ui/icon" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { TextField } from "@opencode-ai/ui/text-field" -import { showToast } from "@opencode-ai/ui/toast" -import fuzzysort from "fuzzysort" -import { formatKeybind, parseKeybind, useCommand } from "@/context/command" -import { useLanguage } from "@/context/language" -import { useSettings } from "@/context/settings" -import { SettingsList } from "./settings-list" - -const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) -const PALETTE_ID = "command.palette" -const DEFAULT_PALETTE_KEYBIND = "mod+shift+p" - -type KeybindGroup = "General" | "Session" | "Navigation" | "Model and agent" | "Terminal" | "Prompt" - -type KeybindMeta = { - title: string - group: KeybindGroup -} - -type KeybindMap = Record -type CommandContext = ReturnType - -const GROUPS: KeybindGroup[] = ["General", "Session", "Navigation", "Model and agent", "Terminal", "Prompt"] - -type GroupKey = - | "settings.shortcuts.group.general" - | "settings.shortcuts.group.session" - | "settings.shortcuts.group.navigation" - | "settings.shortcuts.group.modelAndAgent" - | "settings.shortcuts.group.terminal" - | "settings.shortcuts.group.prompt" - -const groupKey: Record = { - General: "settings.shortcuts.group.general", - Session: "settings.shortcuts.group.session", - Navigation: "settings.shortcuts.group.navigation", - "Model and agent": "settings.shortcuts.group.modelAndAgent", - Terminal: "settings.shortcuts.group.terminal", - Prompt: "settings.shortcuts.group.prompt", -} - -function groupFor(id: string): KeybindGroup { - if (id === PALETTE_ID) return "General" - if (id.startsWith("terminal.")) return "Terminal" - if (id.startsWith("model.") || id.startsWith("agent.") || id.startsWith("mcp.")) return "Model and agent" - if (id.startsWith("file.") || id.startsWith("fileTree.")) return "Navigation" - if (id.startsWith("prompt.")) return "Prompt" - if ( - id.startsWith("session.") || - id.startsWith("message.") || - id.startsWith("permissions.") || - id.startsWith("steps.") || - id.startsWith("review.") - ) - return "Session" - - return "General" -} - -function isModifier(key: string) { - return key === "Shift" || key === "Control" || key === "Alt" || key === "Meta" -} - -function normalizeKey(key: string) { - if (key === ",") return "comma" - if (key === "+") return "plus" - if (key === " ") return "space" - return key.toLowerCase() -} - -function recordKeybind(event: KeyboardEvent) { - if (isModifier(event.key)) return - - const parts: string[] = [] - - const mod = IS_MAC ? event.metaKey : event.ctrlKey - if (mod) parts.push("mod") - - if (IS_MAC && event.ctrlKey) parts.push("ctrl") - if (!IS_MAC && event.metaKey) parts.push("meta") - if (event.altKey) parts.push("alt") - if (event.shiftKey) parts.push("shift") - - const key = normalizeKey(event.key) - if (!key) return - parts.push(key) - - return parts.join("+") -} - -function signatures(config: string | undefined) { - if (!config) return [] - const sigs: string[] = [] - - for (const kb of parseKeybind(config)) { - const parts: string[] = [] - if (kb.ctrl) parts.push("ctrl") - if (kb.alt) parts.push("alt") - if (kb.shift) parts.push("shift") - if (kb.meta) parts.push("meta") - if (kb.key) parts.push(kb.key) - if (parts.length === 0) continue - sigs.push(parts.join("+")) - } - - return sigs -} - -function keybinds(value: unknown): KeybindMap { - if (!value || typeof value !== "object" || Array.isArray(value)) return {} - return value as KeybindMap -} - -function listFor(command: CommandContext, map: KeybindMap, palette: string) { - const out = new Map() - out.set(PALETTE_ID, { title: palette, group: "General" }) - - for (const opt of command.catalog) { - if (opt.id.startsWith("suggested.")) continue - out.set(opt.id, { title: opt.title, group: groupFor(opt.id) }) - } - - for (const opt of command.options) { - if (opt.id.startsWith("suggested.")) continue - out.set(opt.id, { title: opt.title, group: groupFor(opt.id) }) - } - - for (const [id, value] of Object.entries(map)) { - if (typeof value !== "string") continue - if (out.has(id)) continue - out.set(id, { title: id, group: groupFor(id) }) - } - - return out -} - -function groupedFor(list: Map) { - const out = new Map() - for (const group of GROUPS) out.set(group, []) - - for (const [id, item] of list) { - const ids = out.get(item.group) - if (!ids) continue - ids.push(id) - } - - for (const group of GROUPS) { - const ids = out.get(group) - if (!ids) continue - ids.sort((a, b) => (list.get(a)?.title ?? "").localeCompare(list.get(b)?.title ?? "")) - } - - return out -} - -function filteredFor( - query: string, - list: Map, - grouped: Map, - keybind: (id: string) => string, -) { - const value = query.toLowerCase().trim() - if (!value) return grouped - - const out = new Map() - for (const group of GROUPS) out.set(group, []) - - const items = Array.from(list.entries()).map(([id, meta]) => ({ - id, - title: meta.title, - group: meta.group, - keybind: keybind(id), - })) - - const results = fuzzysort.go(value, items, { - keys: ["title", "keybind"], - threshold: -10000, - }) - - for (const result of results) { - const ids = out.get(result.obj.group) - if (!ids) continue - ids.push(result.obj.id) - } - - return out -} - -function useKeyCapture(input: { - active: () => string | null - stop: () => void - set: (id: string, keybind: string) => void - used: () => Map - language: ReturnType -}) { - onMount(() => { - const handle = (event: KeyboardEvent) => { - const id = input.active() - if (!id) return - - event.preventDefault() - event.stopPropagation() - event.stopImmediatePropagation() - - if (event.key === "Escape") { - input.stop() - return - } - - const clear = - (event.key === "Backspace" || event.key === "Delete") && - !event.ctrlKey && - !event.metaKey && - !event.altKey && - !event.shiftKey - if (clear) { - input.set(id, "none") - input.stop() - return - } - - const next = recordKeybind(event) - if (!next) return - - const conflicts = new Map() - for (const sig of signatures(next)) { - for (const item of input.used().get(sig) ?? []) { - if (item.id === id) continue - conflicts.set(item.id, item.title) - } - } - - if (conflicts.size > 0) { - showToast({ - title: input.language.t("settings.shortcuts.conflict.title"), - description: input.language.t("settings.shortcuts.conflict.description", { - keybind: formatKeybind(next, input.language.t), - titles: [...conflicts.values()].join(", "), - }), - }) - return - } - - input.set(id, next) - input.stop() - } - - makeEventListener(document, "keydown", handle, { capture: true }) - }) -} - -export const SettingsKeybinds: Component = () => { - const command = useCommand() - const language = useLanguage() - const settings = useSettings() - - const [store, setStore] = createStore({ - active: null as string | null, - filter: "", - }) - - const stop = () => { - if (!store.active) return - setStore("active", null) - command.keybinds(true) - } - - const start = (id: string) => { - if (store.active === id) { - stop() - return - } - - if (store.active) stop() - - setStore("active", id) - command.keybinds(false) - } - - const map = createMemo(() => keybinds(settings.current.keybinds)) - - const hasOverrides = createMemo(() => Object.values(map()).some((x) => typeof x === "string")) - - const resetAll = () => { - stop() - settings.keybinds.resetAll() - showToast({ - title: language.t("settings.shortcuts.reset.toast.title"), - description: language.t("settings.shortcuts.reset.toast.description"), - }) - } - - const list = createMemo(() => { - language.locale() - return listFor(command, map(), language.t("command.palette")) - }) - - const title = (id: string) => list().get(id)?.title ?? "" - - const grouped = createMemo(() => groupedFor(list())) - - const filtered = createMemo(() => { - return filteredFor(store.filter, list(), grouped(), (id) => command.keybind(id) || "") - }) - - const hasResults = createMemo(() => { - for (const group of GROUPS) { - const ids = filtered().get(group) ?? [] - if (ids.length > 0) return true - } - return false - }) - - const used = createMemo(() => { - const map = new Map() - - const add = (key: string, value: { id: string; title: string }) => { - const list = map.get(key) - if (!list) { - map.set(key, [value]) - return - } - list.push(value) - } - - const palette = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND - for (const sig of signatures(palette)) { - add(sig, { id: PALETTE_ID, title: title(PALETTE_ID) }) - } - - const valueFor = (id: string) => { - const custom = settings.keybinds.get(id) - if (typeof custom === "string") return custom - - const live = command.options.find((x) => x.id === id) - if (live?.keybind) return live.keybind - - const meta = command.catalog.find((x) => x.id === id) - return meta?.keybind - } - - for (const id of list().keys()) { - if (id === PALETTE_ID) continue - for (const sig of signatures(valueFor(id))) { - add(sig, { id, title: title(id) }) - } - } - - return map - }) - - const setKeybind = (id: string, keybind: string) => settings.keybinds.set(id, keybind) - - useKeyCapture({ - active: () => store.active, - stop, - set: setKeybind, - used, - language, - }) - - onCleanup(() => { - if (store.active) command.keybinds(true) - }) - - return ( -
-
-
-
-

{language.t("settings.shortcuts.title")}

- -
- -
- - setStore("filter", v)} - placeholder={language.t("settings.shortcuts.search.placeholder")} - spellcheck={false} - autocorrect="off" - autocomplete="off" - autocapitalize="off" - class="flex-1" - /> - - setStore("filter", "")} /> - -
-
-
- -
- - {(group) => ( - 0}> -
-

{language.t(groupKey[group])}

- - - {(id) => ( -
- {title(id)} - -
- )} -
-
-
-
- )} -
- - -
- {language.t("settings.shortcuts.search.empty")} - - "{store.filter}" - -
-
-
-
- ) -} diff --git a/packages/app/src/components/settings-list.tsx b/packages/app/src/components/settings-list.tsx deleted file mode 100644 index bd8e4d7d1f1..00000000000 --- a/packages/app/src/components/settings-list.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { type Component, type JSX } from "solid-js" - -export const SettingsList: Component<{ children: JSX.Element }> = (props) => { - return
{props.children}
-} diff --git a/packages/app/src/components/settings-models.tsx b/packages/app/src/components/settings-models.tsx deleted file mode 100644 index 14667338e95..00000000000 --- a/packages/app/src/components/settings-models.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { useFilteredList } from "@opencode-ai/ui/hooks" -import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { Switch } from "@opencode-ai/ui/switch" -import { Icon } from "@opencode-ai/ui/icon" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { TextField } from "@opencode-ai/ui/text-field" -import { type Component, For, Show } from "solid-js" -import { useLanguage } from "@/context/language" -import { useModels } from "@/context/models" -import { popularProviders } from "@/hooks/use-providers" -import { SettingsList } from "./settings-list" - -type ModelItem = ReturnType["list"]>[number] - -const ListLoadingState: Component<{ label: string }> = (props) => { - return ( -
- {props.label} -
- ) -} - -const ListEmptyState: Component<{ message: string; filter: string }> = (props) => { - return ( -
- {props.message} - - "{props.filter}" - -
- ) -} - -export const SettingsModels: Component = () => { - const language = useLanguage() - const models = useModels() - - const list = useFilteredList({ - items: (_filter) => models.list(), - key: (x) => `${x.provider.id}:${x.id}`, - filterKeys: ["provider.name", "name", "id"], - sortBy: (a, b) => a.name.localeCompare(b.name), - groupBy: (x) => x.provider.id, - sortGroupsBy: (a, b) => { - const aIndex = popularProviders.indexOf(a.category) - const bIndex = popularProviders.indexOf(b.category) - const aPopular = aIndex >= 0 - const bPopular = bIndex >= 0 - - if (aPopular && !bPopular) return -1 - if (!aPopular && bPopular) return 1 - if (aPopular && bPopular) return aIndex - bIndex - - const aName = a.items[0].provider.name - const bName = b.items[0].provider.name - return aName.localeCompare(bName) - }, - }) - - return ( -
-
-
-

{language.t("settings.models.title")}

-
- - - - - -
-
-
- -
- - } - > - 0} - fallback={} - > - - {(group) => ( -
-
- - {group.items[0].provider.name} -
- - - {(item) => { - const key = { providerID: item.provider.id, modelID: item.id } - return ( -
-
- {item.name} -
-
- { - models.setVisibility(key, checked) - }} - hideLabel - > - {item.name} - -
-
- ) - }} -
-
-
- )} -
-
-
-
-
- ) -} diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx deleted file mode 100644 index cc69327f80d..00000000000 --- a/packages/app/src/components/settings-providers.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import { Button } from "@opencode-ai/ui/button" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { Tag } from "@opencode-ai/ui/tag" -import { showToast } from "@opencode-ai/ui/toast" -import { popularProviders, useProviders } from "@/hooks/use-providers" -import { createMemo, type Component, For, Show } from "solid-js" -import { useLanguage } from "@/context/language" -import { useGlobalSDK } from "@/context/global-sdk" -import { useGlobalSync } from "@/context/global-sync" -import { DialogConnectProvider } from "./dialog-connect-provider" -import { DialogSelectProvider } from "./dialog-select-provider" -import { DialogCustomProvider } from "./dialog-custom-provider" -import { SettingsList } from "./settings-list" - -type ProviderSource = "env" | "api" | "config" | "custom" -type ProviderItem = ReturnType["connected"]>[number] - -const PROVIDER_NOTES = [ - { match: (id: string) => id === "opencode", key: "dialog.provider.opencode.note" }, - { match: (id: string) => id === "opencode-go", key: "dialog.provider.opencodeGo.tagline" }, - { match: (id: string) => id === "anthropic", key: "dialog.provider.anthropic.note" }, - { match: (id: string) => id.startsWith("github-copilot"), key: "dialog.provider.copilot.note" }, - { match: (id: string) => id === "openai", key: "dialog.provider.openai.note" }, - { match: (id: string) => id === "google", key: "dialog.provider.google.note" }, - { match: (id: string) => id === "openrouter", key: "dialog.provider.openrouter.note" }, - { match: (id: string) => id === "vercel", key: "dialog.provider.vercel.note" }, -] as const - -export const SettingsProviders: Component = () => { - const dialog = useDialog() - const language = useLanguage() - const globalSDK = useGlobalSDK() - const globalSync = useGlobalSync() - const providers = useProviders() - - const connected = createMemo(() => { - return providers - .connected() - .filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input)) - }) - - const popular = createMemo(() => { - const connectedIDs = new Set(connected().map((p) => p.id)) - const items = providers - .popular() - .filter((p) => !connectedIDs.has(p.id)) - .slice() - items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)) - return items - }) - - const source = (item: ProviderItem): ProviderSource | undefined => { - if (!("source" in item)) return - const value = item.source - if (value === "env" || value === "api" || value === "config" || value === "custom") return value - return - } - - const type = (item: ProviderItem) => { - const current = source(item) - if (current === "env") return language.t("settings.providers.tag.environment") - if (current === "api") return language.t("provider.connect.method.apiKey") - if (current === "config") { - if (isConfigCustom(item.id)) return language.t("settings.providers.tag.custom") - return language.t("settings.providers.tag.config") - } - if (current === "custom") return language.t("settings.providers.tag.custom") - return language.t("settings.providers.tag.other") - } - - const canDisconnect = (item: ProviderItem) => source(item) !== "env" - - const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key - - const isConfigCustom = (providerID: string) => { - const provider = globalSync.data.config.provider?.[providerID] - if (!provider) return false - if (provider.npm !== "@ai-sdk/openai-compatible") return false - if (!provider.models || Object.keys(provider.models).length === 0) return false - return true - } - - const disableProvider = async (providerID: string, name: string) => { - const before = globalSync.data.config.disabled_providers ?? [] - const next = before.includes(providerID) ? before : [...before, providerID] - globalSync.set("config", "disabled_providers", next) - - await globalSync - .updateConfig({ disabled_providers: next }) - .then(() => { - showToast({ - variant: "success", - icon: "circle-check", - title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }), - description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }), - }) - }) - .catch((err: unknown) => { - globalSync.set("config", "disabled_providers", before) - const message = err instanceof Error ? err.message : String(err) - showToast({ title: language.t("common.requestFailed"), description: message }) - }) - } - - const disconnect = async (providerID: string, name: string) => { - if (isConfigCustom(providerID)) { - await globalSDK.client.auth.remove({ providerID }).catch(() => undefined) - await disableProvider(providerID, name) - return - } - await globalSDK.client.auth - .remove({ providerID }) - .then(async () => { - await globalSDK.client.global.dispose() - showToast({ - variant: "success", - icon: "circle-check", - title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }), - description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }), - }) - }) - .catch((err: unknown) => { - const message = err instanceof Error ? err.message : String(err) - showToast({ title: language.t("common.requestFailed"), description: message }) - }) - } - - return ( -
-
-
-

{language.t("settings.providers.title")}

-
-
- -
-
-

{language.t("settings.providers.section.connected")}

- - 0} - fallback={ -
- {language.t("settings.providers.connected.empty")} -
- } - > - - {(item) => ( -
-
- - {item.name} - {type(item)} -
- - {language.t("settings.providers.connected.environmentDescription")} - - } - > - - -
- )} -
-
-
-
- -
-

{language.t("settings.providers.section.popular")}

- - - {(item) => ( -
-
-
- - {item.name} - - {language.t("dialog.provider.tag.recommended")} - - - {language.t("dialog.provider.tag.recommended")} - -
- - {(key) => {language.t(key())}} - -
- -
- )} -
- -
-
-
- - {language.t("provider.custom.title")} - {language.t("settings.providers.tag.custom")} -
- - {language.t("settings.providers.custom.description")} - -
- -
-
- - -
-
-
- ) -} diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx deleted file mode 100644 index 0f6a1c1355f..00000000000 --- a/packages/app/src/components/status-popover-body.tsx +++ /dev/null @@ -1,445 +0,0 @@ -import { Button } from "@opencode-ai/ui/button" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { Icon } from "@opencode-ai/ui/icon" -import { Switch } from "@opencode-ai/ui/switch" -import { Tabs } from "@opencode-ai/ui/tabs" -import { useMutation } from "@tanstack/solid-query" -import { showToast } from "@opencode-ai/ui/toast" -import { useNavigate } from "@solidjs/router" -import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js" -import { createStore, reconcile } from "solid-js/store" -import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row" -import { useLanguage } from "@/context/language" -import { usePlatform } from "@/context/platform" -import { useSDK } from "@/context/sdk" -import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" -import { useSync } from "@/context/sync" -import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health" - -const pollMs = 10_000 - -const pluginEmptyMessage = (value: string, file: string): JSXElement => { - const parts = value.split(file) - if (parts.length === 1) return value - return ( - <> - {parts[0]} - {file} - {parts.slice(1).join(file)} - - ) -} - -const listServersByHealth = ( - list: ServerConnection.Any[], - active: ServerConnection.Key | undefined, - status: Record, -) => { - if (!list.length) return list - const order = new Map(list.map((url, index) => [url, index] as const)) - const rank = (value?: ServerHealth) => { - if (value?.healthy === true) return 0 - if (value?.healthy === false) return 2 - return 1 - } - - return list.slice().sort((a, b) => { - if (ServerConnection.key(a) === active) return -1 - if (ServerConnection.key(b) === active) return 1 - const diff = rank(status[ServerConnection.key(a)]) - rank(status[ServerConnection.key(b)]) - if (diff !== 0) return diff - return (order.get(a) ?? 0) - (order.get(b) ?? 0) - }) -} - -const useServerHealth = (servers: Accessor, enabled: Accessor) => { - const checkServerHealth = useCheckServerHealth() - const [status, setStatus] = createStore({} as Record) - - createEffect(() => { - if (!enabled()) { - setStatus(reconcile({})) - return - } - const list = servers() - let dead = false - - const refresh = async () => { - const results: Record = {} - await Promise.all( - list.map(async (conn) => { - results[ServerConnection.key(conn)] = await checkServerHealth(conn.http) - }), - ) - if (dead) return - setStatus(reconcile(results)) - } - - void refresh() - const id = setInterval(() => void refresh(), pollMs) - onCleanup(() => { - dead = true - clearInterval(id) - }) - }) - - return status -} - -const useDefaultServerKey = ( - get: (() => string | Promise | null | undefined) | undefined, -) => { - const [state, setState] = createStore({ - url: undefined as string | undefined, - tick: 0, - }) - - createEffect(() => { - state.tick - let dead = false - const result = get?.() - if (!result) { - setState("url", undefined) - onCleanup(() => { - dead = true - }) - return - } - - if (result instanceof Promise) { - void result.then((next) => { - if (dead) return - setState("url", next ? normalizeServerUrl(next) : undefined) - }) - onCleanup(() => { - dead = true - }) - return - } - - setState("url", normalizeServerUrl(result)) - onCleanup(() => { - dead = true - }) - }) - - return { - key: () => { - const u = state.url - if (!u) return - return ServerConnection.key({ type: "http", http: { url: u } }) - }, - refresh: () => setState("tick", (value) => value + 1), - } -} - -const useMcpToggleMutation = () => { - const sync = useSync() - const sdk = useSDK() - const language = useLanguage() - - return useMutation(() => ({ - mutationFn: async (name: string) => { - const status = sync.data.mcp[name] - await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name })) - const result = await sdk.client.mcp.status() - if (result.data) sync.set("mcp", result.data) - }, - onError: (err) => { - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: err instanceof Error ? err.message : String(err), - }) - }, - })) -} - -export function StatusPopoverBody(props: { shown: Accessor }) { - const sync = useSync() - const server = useServer() - const platform = usePlatform() - const dialog = useDialog() - const language = useLanguage() - const navigate = useNavigate() - const sdk = useSDK() - - const [load, setLoad] = createStore({ - lspDone: false, - lspLoading: false, - mcpDone: false, - mcpLoading: false, - }) - - const fail = (err: unknown) => { - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: err instanceof Error ? err.message : String(err), - }) - } - - createEffect(() => { - if (!props.shown()) return - - if (!sync.data.mcp_ready && !load.mcpDone && !load.mcpLoading) { - setLoad("mcpLoading", true) - void sdk.client.mcp - .status() - .then((result) => { - sync.set("mcp", result.data ?? {}) - sync.set("mcp_ready", true) - }) - .catch((err) => { - setLoad("mcpDone", true) - fail(err) - }) - .finally(() => { - setLoad("mcpLoading", false) - }) - } - - if (!sync.data.lsp_ready && !load.lspDone && !load.lspLoading) { - setLoad("lspLoading", true) - void sdk.client.lsp - .status() - .then((result) => { - sync.set("lsp", result.data ?? []) - sync.set("lsp_ready", true) - }) - .catch((err) => { - setLoad("lspDone", true) - fail(err) - }) - .finally(() => { - setLoad("lspLoading", false) - }) - } - }) - - let dialogRun = 0 - let dialogDead = false - onCleanup(() => { - dialogDead = true - dialogRun += 1 - }) - const servers = createMemo(() => { - const current = server.current - const list = server.list - if (!current) return list - if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list] - return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))] - }) - const health = useServerHealth(servers, props.shown) - const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health)) - const toggleMcp = useMcpToggleMutation() - const defaultServer = useDefaultServerKey(platform.getDefaultServer) - const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b))) - const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status - const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length) - const lspItems = createMemo(() => sync.data.lsp ?? []) - const lspCount = createMemo(() => lspItems().length) - const plugins = createMemo(() => - (sync.data.config.plugin ?? []).map((item) => (typeof item === "string" ? item : item[0])), - ) - const pluginCount = createMemo(() => plugins().length) - const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json")) - - return ( -
- - - - {sortedServers().length > 0 ? `${sortedServers().length} ` : ""} - {language.t("status.popover.tab.servers")} - - - {mcpConnected() > 0 ? `${mcpConnected()} ` : ""} - {language.t("status.popover.tab.mcp")} - - - {lspCount() > 0 ? `${lspCount()} ` : ""} - {language.t("status.popover.tab.lsp")} - - - {pluginCount() > 0 ? `${pluginCount()} ` : ""} - {language.t("status.popover.tab.plugins")} - - - - -
-
- - {(s) => { - const key = ServerConnection.key(s) - const blocked = () => health[key]?.healthy === false - return ( - - ) - }} - - - -
-
-
- - -
-
- 0} - fallback={ -
{language.t("dialog.mcp.empty")}
- } - > - - {(name) => { - const status = () => mcpStatus(name) - const enabled = () => status() === "connected" - return ( - - ) - }} - -
-
-
-
- - -
-
- 0} - fallback={ -
{language.t("dialog.lsp.empty")}
- } - > - - {(item) => ( -
-
- {item.name || item.id} -
- )} - - -
-
- - - -
-
- 0} - fallback={
{pluginEmpty()}
} - > - - {(plugin) => ( -
-
- {plugin} -
- )} - - -
-
- - -
- ) -} diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx deleted file mode 100644 index 6820a940b05..00000000000 --- a/packages/app/src/components/status-popover.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Button } from "@opencode-ai/ui/button" -import { Icon } from "@opencode-ai/ui/icon" -import { Popover } from "@opencode-ai/ui/popover" -import { Suspense, createMemo, createSignal, lazy, Show } from "solid-js" -import { useLanguage } from "@/context/language" -import { useServer } from "@/context/server" -import { useSync } from "@/context/sync" - -const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody }))) - -export function StatusPopover() { - const language = useLanguage() - const server = useServer() - const sync = useSync() - const [shown, setShown] = createSignal(false) - const ready = createMemo(() => server.healthy() === false || sync.data.mcp_ready) - const healthy = createMemo(() => { - const serverHealthy = server.healthy() === true - const mcp = Object.values(sync.data.mcp ?? {}) - const issue = mcp.some((item) => item.status !== "connected" && item.status !== "disabled") - return serverHealthy && !issue - }) - - return ( - -
- -
-
-
- } - class="[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl" - gutter={4} - placement="bottom-end" - shift={-168} - > - - - } - > - - - -
- ) -} diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx deleted file mode 100644 index ff5ff9dada8..00000000000 --- a/packages/app/src/components/terminal.tsx +++ /dev/null @@ -1,645 +0,0 @@ -import { withAlpha } from "@opencode-ai/ui/theme/color" -import { useTheme } from "@opencode-ai/ui/theme/context" -import { resolveThemeVariant } from "@opencode-ai/ui/theme/resolve" -import type { HexColor } from "@opencode-ai/ui/theme/types" -import { showToast } from "@opencode-ai/ui/toast" -import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web" -import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js" -import { SerializeAddon } from "@/addons/serialize" -import { matchKeybind, parseKeybind } from "@/context/command" -import { useLanguage } from "@/context/language" -import { usePlatform } from "@/context/platform" -import { useSDK } from "@/context/sdk" -import { useServer } from "@/context/server" -import { terminalFontFamily, useSettings } from "@/context/settings" -import type { LocalPTY } from "@/context/terminal" -import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters" -import { terminalWriter } from "@/utils/terminal-writer" - -const TOGGLE_TERMINAL_ID = "terminal.toggle" -const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`" -export interface TerminalProps extends ComponentProps<"div"> { - pty: LocalPTY - autoFocus?: boolean - onSubmit?: () => void - onCleanup?: (pty: Partial & { id: string }) => void - onConnect?: () => void - onConnectError?: (error: unknown) => void -} - -let shared: Promise<{ mod: typeof import("ghostty-web"); ghostty: Ghostty }> | undefined - -const loadGhostty = () => { - if (shared) return shared - shared = import("ghostty-web") - .then(async (mod) => ({ mod, ghostty: await mod.Ghostty.load() })) - .catch((err) => { - shared = undefined - throw err - }) - return shared -} - -type TerminalColors = { - background: string - foreground: string - cursor: string - selectionBackground: string -} - -const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = { - light: { - background: "#fcfcfc", - foreground: "#211e1e", - cursor: "#211e1e", - selectionBackground: withAlpha("#211e1e", 0.2), - }, - dark: { - background: "#191515", - foreground: "#d4d4d4", - cursor: "#d4d4d4", - selectionBackground: withAlpha("#d4d4d4", 0.25), - }, -} - -const debugTerminal = (...values: unknown[]) => { - if (!import.meta.env.DEV) return - console.debug("[terminal]", ...values) -} - -const errorName = (err: unknown) => { - if (!err || typeof err !== "object") return - if (!("name" in err)) return - const errorName = err.name - return typeof errorName === "string" ? errorName : undefined -} - -const useTerminalUiBindings = (input: { - container: HTMLDivElement - term: Term - cleanups: VoidFunction[] - handlePointerDown: () => void - handleLinkClick: (event: MouseEvent) => void -}) => { - const handleCopy = (event: ClipboardEvent) => { - const selection = input.term.getSelection() - if (!selection) return - - const clipboard = event.clipboardData - if (!clipboard) return - - event.preventDefault() - clipboard.setData("text/plain", selection) - } - - const handlePaste = (event: ClipboardEvent) => { - const clipboard = event.clipboardData - const text = clipboard?.getData("text/plain") ?? clipboard?.getData("text") ?? "" - if (!text) return - - event.preventDefault() - event.stopPropagation() - input.term.paste(text) - } - - const handleTextareaFocus = () => { - input.term.options.cursorBlink = true - } - const handleTextareaBlur = () => { - input.term.options.cursorBlink = false - } - - input.container.addEventListener("copy", handleCopy, true) - input.cleanups.push(() => input.container.removeEventListener("copy", handleCopy, true)) - - input.container.addEventListener("paste", handlePaste, true) - input.cleanups.push(() => input.container.removeEventListener("paste", handlePaste, true)) - - input.container.addEventListener("pointerdown", input.handlePointerDown) - input.cleanups.push(() => input.container.removeEventListener("pointerdown", input.handlePointerDown)) - - input.container.addEventListener("click", input.handleLinkClick, { - capture: true, - }) - input.cleanups.push(() => - input.container.removeEventListener("click", input.handleLinkClick, { - capture: true, - }), - ) - - input.term.textarea?.addEventListener("focus", handleTextareaFocus) - input.term.textarea?.addEventListener("blur", handleTextareaBlur) - input.cleanups.push(() => input.term.textarea?.removeEventListener("focus", handleTextareaFocus)) - input.cleanups.push(() => input.term.textarea?.removeEventListener("blur", handleTextareaBlur)) -} - -const persistTerminal = (input: { - term: Term | undefined - addon: SerializeAddon | undefined - cursor: number - id: string - onCleanup?: (pty: Partial & { id: string }) => void -}) => { - if (!input.addon || !input.onCleanup || !input.term) return - const buffer = (() => { - try { - return input.addon.serialize() - } catch { - debugTerminal("failed to serialize terminal buffer") - return "" - } - })() - - input.onCleanup({ - id: input.id, - buffer, - cursor: input.cursor, - rows: input.term.rows, - cols: input.term.cols, - scrollY: input.term.getViewportY(), - }) -} - -export const Terminal = (props: TerminalProps) => { - const platform = usePlatform() - const sdk = useSDK() - const settings = useSettings() - const theme = useTheme() - const language = useLanguage() - const server = useServer() - const directory = sdk.directory - const client = sdk.client - const url = sdk.url - const auth = server.current?.http - const username = auth?.username ?? "opencode" - const password = auth?.password ?? "" - const sameOrigin = new URL(url, location.href).origin === location.origin - let container!: HTMLDivElement - const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"]) - const id = local.pty.id - const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : "" - const restoreSize = - restore && - typeof local.pty.cols === "number" && - Number.isSafeInteger(local.pty.cols) && - local.pty.cols > 0 && - typeof local.pty.rows === "number" && - Number.isSafeInteger(local.pty.rows) && - local.pty.rows > 0 - ? { cols: local.pty.cols, rows: local.pty.rows } - : undefined - const scrollY = typeof local.pty.scrollY === "number" ? local.pty.scrollY : undefined - let ws: WebSocket | undefined - let term: Term | undefined - let _ghostty: Ghostty - let serializeAddon: SerializeAddon - let fitAddon: FitAddon - let handleResize: () => void - let fitFrame: number | undefined - let sizeTimer: ReturnType | undefined - let pendingSize: { cols: number; rows: number } | undefined - let lastSize: { cols: number; rows: number } | undefined - let disposed = false - const cleanups: VoidFunction[] = [] - const start = - typeof local.pty.cursor === "number" && Number.isSafeInteger(local.pty.cursor) ? local.pty.cursor : undefined - let cursor = start ?? 0 - let seek = start !== undefined ? start : restore ? -1 : 0 - let output: ReturnType | undefined - let drop: VoidFunction | undefined - let reconn: ReturnType | undefined - let tries = 0 - - const cleanup = () => { - if (!cleanups.length) return - const fns = cleanups.splice(0).reverse() - for (const fn of fns) { - try { - fn() - } catch (err) { - debugTerminal("cleanup failed", err) - } - } - } - - const pushSize = (cols: number, rows: number) => { - return client.pty - .update({ - ptyID: id, - size: { cols, rows }, - }) - .catch((err) => { - debugTerminal("failed to sync terminal size", err) - }) - } - - const getTerminalColors = (): TerminalColors => { - const mode = theme.mode() === "dark" ? "dark" : "light" - const fallback = DEFAULT_TERMINAL_COLORS[mode] - const currentTheme = theme.themes()[theme.themeId()] - if (!currentTheme) return fallback - const variant = mode === "dark" ? currentTheme.dark : currentTheme.light - if (!variant?.seeds && !variant?.palette) return fallback - const resolved = resolveThemeVariant(variant, mode === "dark") - const text = resolved["text-stronger"] ?? fallback.foreground - const background = resolved["background-stronger"] ?? fallback.background - const alpha = mode === "dark" ? 0.25 : 0.2 - const base = text.startsWith("#") ? (text as HexColor) : (fallback.foreground as HexColor) - const selectionBackground = withAlpha(base, alpha) - return { - background, - foreground: text, - cursor: text, - selectionBackground, - } - } - - const terminalColors = createMemo(getTerminalColors) - - const scheduleFit = () => { - if (disposed) return - if (!fitAddon) return - if (fitFrame !== undefined) return - - fitFrame = requestAnimationFrame(() => { - fitFrame = undefined - if (disposed) return - fitAddon.fit() - }) - } - - const scheduleSize = (cols: number, rows: number) => { - if (disposed) return - if (lastSize?.cols === cols && lastSize?.rows === rows) return - - pendingSize = { cols, rows } - - if (!lastSize) { - lastSize = pendingSize - void pushSize(cols, rows) - return - } - - if (sizeTimer !== undefined) return - sizeTimer = setTimeout(() => { - sizeTimer = undefined - const next = pendingSize - if (!next) return - pendingSize = undefined - if (disposed) return - if (lastSize?.cols === next.cols && lastSize?.rows === next.rows) return - lastSize = next - void pushSize(next.cols, next.rows) - }, 100) - } - - createEffect(() => { - const colors = terminalColors() - if (!term) return - setOptionIfSupported(term, "theme", colors) - }) - - createEffect(() => { - const font = terminalFontFamily(settings.appearance.terminalFont()) - if (!term) return - setOptionIfSupported(term, "fontFamily", font) - scheduleFit() - }) - - let zoom = platform.webviewZoom?.() - createEffect(() => { - const next = platform.webviewZoom?.() - if (next === undefined) return - if (next === zoom) return - zoom = next - scheduleFit() - }) - - const focusTerminal = () => { - const t = term - if (!t) return - t.focus() - t.textarea?.focus() - setTimeout(() => t.textarea?.focus(), 0) - } - const handlePointerDown = () => { - const activeElement = document.activeElement - if (activeElement instanceof HTMLElement && activeElement !== container && !container.contains(activeElement)) { - activeElement.blur() - } - focusTerminal() - } - - const handleLinkClick = (event: MouseEvent) => { - if (!event.shiftKey && !event.ctrlKey && !event.metaKey) return - if (event.altKey) return - if (event.button !== 0) return - - const t = term - if (!t) return - - const text = getHoveredLinkText(t) - if (!text) return - - event.preventDefault() - event.stopImmediatePropagation() - platform.openLink(text) - } - - onMount(() => { - const run = async () => { - const loaded = await loadGhostty() - if (disposed) return - - const mod = loaded.mod - const g = loaded.ghostty - - const t = new mod.Terminal({ - cursorBlink: true, - cursorStyle: "bar", - cols: restoreSize?.cols, - rows: restoreSize?.rows, - fontSize: 14, - fontFamily: terminalFontFamily(settings.appearance.terminalFont()), - allowTransparency: false, - convertEol: false, - theme: terminalColors(), - scrollback: 10_000, - ghostty: g, - }) - cleanups.push(() => t.dispose()) - if (disposed) { - cleanup() - return - } - _ghostty = g - term = t - output = terminalWriter((data, done) => - t.write(data, () => { - done?.() - }), - ) - - t.attachCustomKeyEventHandler((event) => { - const key = event.key.toLowerCase() - - if (event.ctrlKey && event.shiftKey && !event.metaKey && key === "c") { - document.execCommand("copy") - return true - } - - // allow for toggle terminal keybinds in parent - const config = settings.keybinds.get(TOGGLE_TERMINAL_ID) ?? DEFAULT_TOGGLE_TERMINAL_KEYBIND - const keybinds = parseKeybind(config) - - return matchKeybind(keybinds, event) - }) - - const fit = new mod.FitAddon() - const serializer = new SerializeAddon() - cleanups.push(() => disposeIfDisposable(fit)) - t.loadAddon(serializer) - t.loadAddon(fit) - fitAddon = fit - serializeAddon = serializer - - t.open(container) - useTerminalUiBindings({ - container, - term: t, - cleanups, - handlePointerDown, - handleLinkClick, - }) - - if (local.autoFocus !== false) focusTerminal() - - if (typeof document !== "undefined" && document.fonts) { - void document.fonts.ready.then(scheduleFit) - } - - const onResize = t.onResize((size) => { - scheduleSize(size.cols, size.rows) - }) - cleanups.push(() => disposeIfDisposable(onResize)) - const onData = t.onData((data) => { - if (ws?.readyState === WebSocket.OPEN) ws.send(data) - }) - cleanups.push(() => disposeIfDisposable(onData)) - const onKey = t.onKey((key) => { - if (key.key == "Enter") { - props.onSubmit?.() - } - }) - cleanups.push(() => disposeIfDisposable(onKey)) - - const startResize = () => { - fit.observeResize() - handleResize = scheduleFit - window.addEventListener("resize", handleResize) - cleanups.push(() => window.removeEventListener("resize", handleResize)) - } - - const write = (data: string) => - new Promise((resolve) => { - if (!output) { - resolve() - return - } - output.push(data) - output.flush(resolve) - }) - - if (restore && restoreSize) { - await write(restore) - fit.fit() - scheduleSize(t.cols, t.rows) - if (scrollY !== undefined) t.scrollToLine(scrollY) - startResize() - } else { - fit.fit() - scheduleSize(t.cols, t.rows) - if (restore) { - await write(restore) - if (scrollY !== undefined) t.scrollToLine(scrollY) - } - startResize() - } - - const once = { value: false } - const decoder = new TextDecoder() - - const fail = (err: unknown) => { - if (disposed) return - if (once.value) return - once.value = true - local.onConnectError?.(err) - } - - const gone = () => - client.pty - .get({ ptyID: id }) - .then(() => false) - .catch((err) => { - if (errorName(err) === "NotFoundError") return true - debugTerminal("failed to inspect terminal session", err) - return false - }) - - const retry = (err: unknown) => { - if (disposed) return - if (reconn !== undefined) return - - const ms = Math.min(250 * 2 ** Math.min(tries, 4), 4_000) - reconn = setTimeout(async () => { - reconn = undefined - if (disposed) return - if (await gone()) { - if (disposed) return - fail(err) - return - } - if (disposed) return - tries += 1 - open() - }, ms) - } - - const open = () => { - if (disposed) return - drop?.() - - const next = new URL(url + `/pty/${id}/connect`) - next.searchParams.set("directory", directory) - next.searchParams.set("cursor", String(seek)) - next.protocol = next.protocol === "https:" ? "wss:" : "ws:" - if (!sameOrigin && password) { - next.searchParams.set("auth_token", btoa(`${username}:${password}`)) - // For same-origin requests, let the browser reuse the page's existing auth. - next.username = username - next.password = password - } - - const socket = new WebSocket(next) - socket.binaryType = "arraybuffer" - ws = socket - - const handleOpen = () => { - if (disposed) return - tries = 0 - local.onConnect?.() - scheduleSize(t.cols, t.rows) - } - - const handleMessage = (event: MessageEvent) => { - if (disposed) return - if (event.data instanceof ArrayBuffer) { - const bytes = new Uint8Array(event.data) - if (bytes[0] !== 0) return - const json = decoder.decode(bytes.subarray(1)) - try { - const meta = JSON.parse(json) as { cursor?: unknown } - const next = meta?.cursor - if (typeof next === "number" && Number.isSafeInteger(next) && next >= 0) { - cursor = next - seek = next - } - } catch (err) { - debugTerminal("invalid websocket control frame", err) - } - return - } - - const data = typeof event.data === "string" ? event.data : "" - if (!data) return - output?.push(data) - cursor += data.length - seek = cursor - } - - const handleError = (error: Event) => { - if (disposed) return - debugTerminal("websocket error", error) - } - - const stop = () => { - socket.removeEventListener("open", handleOpen) - socket.removeEventListener("message", handleMessage) - socket.removeEventListener("error", handleError) - socket.removeEventListener("close", handleClose) - if (ws === socket) ws = undefined - if (drop === stop) drop = undefined - if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close(1000) - } - - const handleClose = (event: CloseEvent) => { - if (ws === socket) ws = undefined - if (drop === stop) drop = undefined - socket.removeEventListener("open", handleOpen) - socket.removeEventListener("message", handleMessage) - socket.removeEventListener("error", handleError) - socket.removeEventListener("close", handleClose) - if (disposed) return - if (event.code === 1000) return - retry(new Error(language.t("terminal.connectionLost.abnormalClose", { code: event.code }))) - } - - drop = stop - socket.addEventListener("open", handleOpen) - socket.addEventListener("message", handleMessage) - socket.addEventListener("error", handleError) - socket.addEventListener("close", handleClose) - } - - open() - } - - void run().catch((err) => { - if (disposed) return - showToast({ - variant: "error", - title: language.t("terminal.connectionLost.title"), - description: err instanceof Error ? err.message : language.t("terminal.connectionLost.description"), - }) - local.onConnectError?.(err) - }) - }) - - onCleanup(() => { - disposed = true - if (fitFrame !== undefined) cancelAnimationFrame(fitFrame) - if (sizeTimer !== undefined) clearTimeout(sizeTimer) - if (reconn !== undefined) clearTimeout(reconn) - drop?.() - if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close(1000) - - const finalize = () => { - persistTerminal({ term, addon: serializeAddon, cursor, id, onCleanup: props.onCleanup }) - cleanup() - } - - if (!output) { - finalize() - return - } - - output.flush(finalize) - }) - - return ( -
- ) -} diff --git a/packages/app/src/components/titlebar-history.test.ts b/packages/app/src/components/titlebar-history.test.ts deleted file mode 100644 index 25035d7ccf7..00000000000 --- a/packages/app/src/components/titlebar-history.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { applyPath, backPath, forwardPath, type TitlebarHistory } from "./titlebar-history" - -function history(): TitlebarHistory { - return { stack: [], index: 0, action: undefined } -} - -describe("titlebar history", () => { - test("append and trim keeps max bounded", () => { - let state = history() - state = applyPath(state, "/", 3) - state = applyPath(state, "/a", 3) - state = applyPath(state, "/b", 3) - state = applyPath(state, "/c", 3) - - expect(state.stack).toEqual(["/a", "/b", "/c"]) - expect(state.stack.length).toBe(3) - expect(state.index).toBe(2) - }) - - test("back and forward indexes stay correct after trimming", () => { - let state = history() - state = applyPath(state, "/", 3) - state = applyPath(state, "/a", 3) - state = applyPath(state, "/b", 3) - state = applyPath(state, "/c", 3) - - expect(state.stack).toEqual(["/a", "/b", "/c"]) - expect(state.index).toBe(2) - - const back = backPath(state) - expect(back?.to).toBe("/b") - expect(back?.state.index).toBe(1) - - const afterBack = applyPath(back!.state, back!.to, 3) - expect(afterBack.stack).toEqual(["/a", "/b", "/c"]) - expect(afterBack.index).toBe(1) - - const forward = forwardPath(afterBack) - expect(forward?.to).toBe("/c") - expect(forward?.state.index).toBe(2) - - const afterForward = applyPath(forward!.state, forward!.to, 3) - expect(afterForward.stack).toEqual(["/a", "/b", "/c"]) - expect(afterForward.index).toBe(2) - }) - - test("action-driven navigation does not push duplicate history entries", () => { - const state: TitlebarHistory = { - stack: ["/", "/a", "/b"], - index: 2, - action: undefined, - } - - const back = backPath(state) - expect(back?.to).toBe("/a") - - const next = applyPath(back!.state, back!.to, 10) - expect(next.stack).toEqual(["/", "/a", "/b"]) - expect(next.index).toBe(1) - expect(next.action).toBeUndefined() - }) -}) diff --git a/packages/app/src/components/titlebar-history.ts b/packages/app/src/components/titlebar-history.ts deleted file mode 100644 index 44dbbfa3a49..00000000000 --- a/packages/app/src/components/titlebar-history.ts +++ /dev/null @@ -1,57 +0,0 @@ -export const MAX_TITLEBAR_HISTORY = 100 - -export type TitlebarAction = "back" | "forward" | undefined - -export type TitlebarHistory = { - stack: string[] - index: number - action: TitlebarAction -} - -export function applyPath(state: TitlebarHistory, current: string, max = MAX_TITLEBAR_HISTORY): TitlebarHistory { - if (!state.stack.length) { - const stack = current === "/" ? ["/"] : ["/", current] - return { stack, index: stack.length - 1, action: undefined } - } - - const active = state.stack[state.index] - if (current === active) { - if (!state.action) return state - return { ...state, action: undefined } - } - - if (state.action) return { ...state, action: undefined } - - return pushPath(state, current, max) -} - -export function pushPath(state: TitlebarHistory, path: string, max = MAX_TITLEBAR_HISTORY): TitlebarHistory { - const stack = state.stack.slice(0, state.index + 1).concat(path) - const next = trimHistory(stack, stack.length - 1, max) - return { ...state, ...next, action: undefined } -} - -export function trimHistory(stack: string[], index: number, max = MAX_TITLEBAR_HISTORY) { - if (stack.length <= max) return { stack, index } - const cut = stack.length - max - return { - stack: stack.slice(cut), - index: Math.max(0, index - cut), - } -} - -export function backPath(state: TitlebarHistory) { - if (state.index <= 0) return - const index = state.index - 1 - const to = state.stack[index] - if (!to) return - return { state: { ...state, index, action: "back" as const }, to } -} - -export function forwardPath(state: TitlebarHistory) { - if (state.index >= state.stack.length - 1) return - const index = state.index + 1 - const to = state.stack[index] - if (!to) return - return { state: { ...state, index, action: "forward" as const }, to } -} diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx deleted file mode 100644 index 29e9828a693..00000000000 --- a/packages/app/src/components/titlebar.tsx +++ /dev/null @@ -1,322 +0,0 @@ -import { createEffect, createMemo, Show, untrack } from "solid-js" -import { createStore } from "solid-js/store" -import { useLocation, useNavigate, useParams } from "@solidjs/router" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Icon } from "@opencode-ai/ui/icon" -import { Button } from "@opencode-ai/ui/button" -import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { useTheme } from "@opencode-ai/ui/theme/context" - -import { useLayout } from "@/context/layout" -import { usePlatform } from "@/context/platform" -import { useCommand } from "@/context/command" -import { useLanguage } from "@/context/language" -import { useSettings } from "@/context/settings" -import { applyPath, backPath, forwardPath } from "./titlebar-history" - -type TauriDesktopWindow = { - startDragging?: () => Promise - toggleMaximize?: () => Promise -} - -type TauriThemeWindow = { - setTheme?: (theme?: "light" | "dark" | null) => Promise -} - -type TauriApi = { - window?: { - getCurrentWindow?: () => TauriDesktopWindow - } - webviewWindow?: { - getCurrentWebviewWindow?: () => TauriThemeWindow - } -} - -const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__ -const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.() -const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.() - -export function Titlebar() { - const layout = useLayout() - const platform = usePlatform() - const command = useCommand() - const language = useLanguage() - const settings = useSettings() - const theme = useTheme() - const navigate = useNavigate() - const location = useLocation() - const params = useParams() - - const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos") - const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows") - const web = createMemo(() => platform.platform === "web") - const zoom = () => platform.webviewZoom?.() ?? 1 - const minHeight = () => (mac() ? `${40 / zoom()}px` : undefined) - - const [history, setHistory] = createStore({ - stack: [] as string[], - index: 0, - action: undefined as "back" | "forward" | undefined, - }) - - const path = () => `${location.pathname}${location.search}${location.hash}` - const creating = createMemo(() => { - if (!params.dir) return false - if (params.id) return false - const parts = location.pathname.replace(/\/+$/, "").split("/") - return parts.at(-1) === "session" - }) - - createEffect(() => { - const current = path() - - untrack(() => { - const next = applyPath(history, current) - if (next === history) return - setHistory(next) - }) - }) - - const canBack = createMemo(() => history.index > 0) - const canForward = createMemo(() => history.index < history.stack.length - 1) - const hasProjects = createMemo(() => layout.projects.list().length > 0) - const nav = createMemo(() => import.meta.env.VITE_KILO_CHANNEL !== "beta" || settings.general.showNavigation()) - - const back = () => { - const next = backPath(history) - if (!next) return - setHistory(next.state) - navigate(next.to) - } - - const forward = () => { - const next = forwardPath(history) - if (!next) return - setHistory(next.state) - navigate(next.to) - } - - command.register(() => [ - { - id: "common.goBack", - title: language.t("common.goBack"), - category: language.t("command.category.view"), - keybind: "mod+[", - onSelect: back, - }, - { - id: "common.goForward", - title: language.t("common.goForward"), - category: language.t("command.category.view"), - keybind: "mod+]", - onSelect: forward, - }, - ]) - - const getWin = () => { - if (platform.platform !== "desktop") return - return currentDesktopWindow() - } - - createEffect(() => { - if (platform.platform !== "desktop") return - - const scheme = theme.colorScheme() - const value = scheme === "system" ? null : scheme - - const win = currentThemeWindow() - if (!win?.setTheme) return - - void win.setTheme(value).catch(() => undefined) - }) - - const interactive = (target: EventTarget | null) => { - if (!(target instanceof Element)) return false - - const selector = - "button, a, input, textarea, select, option, [role='button'], [role='menuitem'], [contenteditable='true'], [contenteditable='']" - - return !!target.closest(selector) - } - - const drag = (e: MouseEvent) => { - if (platform.platform !== "desktop") return - if (e.buttons !== 1) return - if (interactive(e.target)) return - - const win = getWin() - if (!win?.startDragging) return - - e.preventDefault() - void win.startDragging().catch(() => undefined) - } - - const maximize = (e: MouseEvent) => { - if (platform.platform !== "desktop") return - if (interactive(e.target)) return - if (e.target instanceof Element && e.target.closest("[data-tauri-decorum-tb]")) return - - const win = getWin() - if (!win?.toggleMaximize) return - - e.preventDefault() - void win.toggleMaximize().catch(() => undefined) - } - - return ( -
-
- -
-
- -
- - -
- -
-
-
- - - - -
- -
-
-
- -
-
- - {!tauriApi() &&
} -
- -
-
- ) -} diff --git a/packages/app/src/constants/file-picker.ts b/packages/app/src/constants/file-picker.ts deleted file mode 100644 index c661bc8f36b..00000000000 --- a/packages/app/src/constants/file-picker.ts +++ /dev/null @@ -1,89 +0,0 @@ -export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"] - -export const ACCEPTED_FILE_TYPES = [ - ...ACCEPTED_IMAGE_TYPES, - "application/pdf", - "text/*", - "application/json", - "application/ld+json", - "application/toml", - "application/x-toml", - "application/x-yaml", - "application/xml", - "application/yaml", - ".c", - ".cc", - ".cjs", - ".conf", - ".cpp", - ".css", - ".csv", - ".cts", - ".env", - ".go", - ".gql", - ".graphql", - ".h", - ".hh", - ".hpp", - ".htm", - ".html", - ".ini", - ".java", - ".js", - ".json", - ".jsx", - ".log", - ".md", - ".mdx", - ".mjs", - ".mts", - ".py", - ".rb", - ".rs", - ".sass", - ".scss", - ".sh", - ".sql", - ".toml", - ".ts", - ".tsx", - ".txt", - ".xml", - ".yaml", - ".yml", - ".zsh", -] - -const MIME_EXT = new Map([ - ["image/png", "png"], - ["image/jpeg", "jpg"], - ["image/gif", "gif"], - ["image/webp", "webp"], - ["application/pdf", "pdf"], - ["application/json", "json"], - ["application/ld+json", "jsonld"], - ["application/toml", "toml"], - ["application/x-toml", "toml"], - ["application/x-yaml", "yaml"], - ["application/xml", "xml"], - ["application/yaml", "yaml"], -]) - -const TEXT_EXT = ["txt", "text", "md", "markdown", "log", "csv"] - -export const ACCEPTED_FILE_EXTENSIONS = Array.from( - new Set( - ACCEPTED_FILE_TYPES.flatMap((item) => { - if (item.startsWith(".")) return [item.slice(1)] - if (item === "text/*") return TEXT_EXT - const out = MIME_EXT.get(item) - return out ? [out] : [] - }), - ), -).sort() - -export function filePickerFilters(ext?: string[]) { - if (!ext || ext.length === 0) return undefined - return [{ name: "Files", extensions: ext }] -} diff --git a/packages/app/src/context/command-keybind.test.ts b/packages/app/src/context/command-keybind.test.ts deleted file mode 100644 index c8e2dbb5d0f..00000000000 --- a/packages/app/src/context/command-keybind.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { formatKeybind, matchKeybind, parseKeybind } from "./command" - -describe("command keybind helpers", () => { - test("parseKeybind handles aliases and multiple combos", () => { - const keybinds = parseKeybind("control+option+k, mod+shift+comma") - - expect(keybinds).toHaveLength(2) - expect(keybinds[0]).toEqual({ - key: "k", - ctrl: true, - meta: false, - shift: false, - alt: true, - }) - expect(keybinds[1]?.shift).toBe(true) - expect(keybinds[1]?.key).toBe("comma") - expect(Boolean(keybinds[1]?.ctrl || keybinds[1]?.meta)).toBe(true) - }) - - test("parseKeybind treats none and empty as disabled", () => { - expect(parseKeybind("none")).toEqual([]) - expect(parseKeybind("")).toEqual([]) - }) - - test("matchKeybind normalizes punctuation keys", () => { - const keybinds = parseKeybind("ctrl+comma, shift+plus, meta+space") - - expect(matchKeybind(keybinds, new KeyboardEvent("keydown", { key: ",", ctrlKey: true }))).toBe(true) - expect(matchKeybind(keybinds, new KeyboardEvent("keydown", { key: "+", shiftKey: true }))).toBe(true) - expect(matchKeybind(keybinds, new KeyboardEvent("keydown", { key: " ", metaKey: true }))).toBe(true) - expect(matchKeybind(keybinds, new KeyboardEvent("keydown", { key: ",", ctrlKey: true, altKey: true }))).toBe(false) - }) - - test("matchKeybind supports bracket keys", () => { - const keybinds = parseKeybind("mod+alt+[, mod+alt+]") - const prev = keybinds[0] - const next = keybinds[1] - - expect( - matchKeybind( - keybinds, - new KeyboardEvent("keydown", { key: "[", ctrlKey: prev?.ctrl, metaKey: prev?.meta, altKey: true }), - ), - ).toBe(true) - expect( - matchKeybind( - keybinds, - new KeyboardEvent("keydown", { key: "]", ctrlKey: next?.ctrl, metaKey: next?.meta, altKey: true }), - ), - ).toBe(true) - }) - - test("formatKeybind returns human readable output", () => { - const display = formatKeybind("ctrl+alt+arrowup") - - expect(display).toContain("↑") - expect(display.includes("Ctrl") || display.includes("⌃")).toBe(true) - expect(display.includes("Alt") || display.includes("⌥")).toBe(true) - expect(formatKeybind("none")).toBe("") - }) - - test("formatKeybind prefers the first combo", () => { - const display = formatKeybind("mod+k,mod+p") - - expect(display.includes("K") || display.includes("k")).toBe(true) - expect(display.includes("P") || display.includes("p")).toBe(false) - }) -}) diff --git a/packages/app/src/context/command.test.ts b/packages/app/src/context/command.test.ts deleted file mode 100644 index 2b956287c54..00000000000 --- a/packages/app/src/context/command.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { upsertCommandRegistration } from "./command" - -describe("upsertCommandRegistration", () => { - test("replaces keyed registrations", () => { - const one = () => [{ id: "one", title: "One" }] - const two = () => [{ id: "two", title: "Two" }] - - const next = upsertCommandRegistration([{ key: "layout", options: one }], { key: "layout", options: two }) - - expect(next).toHaveLength(1) - expect(next[0]?.options).toBe(two) - }) - - test("keeps unkeyed registrations additive", () => { - const one = () => [{ id: "one", title: "One" }] - const two = () => [{ id: "two", title: "Two" }] - - const next = upsertCommandRegistration([{ options: one }], { options: two }) - - expect(next).toHaveLength(2) - expect(next[0]?.options).toBe(two) - expect(next[1]?.options).toBe(one) - }) -}) diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx deleted file mode 100644 index d2238828c69..00000000000 --- a/packages/app/src/context/command.tsx +++ /dev/null @@ -1,434 +0,0 @@ -import { createSimpleContext } from "@opencode-ai/ui/context" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { type Accessor, createEffect, createMemo, onCleanup, onMount } from "solid-js" -import { createStore } from "solid-js/store" -import { makeEventListener } from "@solid-primitives/event-listener" -import { useLanguage } from "@/context/language" -import { useSettings } from "@/context/settings" -import { dict as en } from "@/i18n/en" -import { Persist, persisted } from "@/utils/persist" - -const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) - -const PALETTE_ID = "command.palette" -const DEFAULT_PALETTE_KEYBIND = "mod+shift+p" -const SUGGESTED_PREFIX = "suggested." -const EDITABLE_KEYBIND_IDS = new Set(["terminal.toggle", "terminal.new", "file.attach"]) - -type KeyLabel = - | "common.key.ctrl" - | "common.key.alt" - | "common.key.shift" - | "common.key.meta" - | "common.key.space" - | "common.key.backspace" - | "common.key.enter" - | "common.key.tab" - | "common.key.delete" - | "common.key.home" - | "common.key.end" - | "common.key.pageUp" - | "common.key.pageDown" - | "common.key.insert" - | "common.key.esc" - -function keyText(key: KeyLabel, t?: (key: KeyLabel) => string) { - return t ? t(key) : en[key] -} - -function actionId(id: string) { - if (!id.startsWith(SUGGESTED_PREFIX)) return id - return id.slice(SUGGESTED_PREFIX.length) -} - -function normalizeKey(key: string) { - if (key === ",") return "comma" - if (key === "+") return "plus" - if (key === " ") return "space" - return key.toLowerCase() -} - -function signature(key: string, ctrl: boolean, meta: boolean, shift: boolean, alt: boolean) { - const mask = (ctrl ? 1 : 0) | (meta ? 2 : 0) | (shift ? 4 : 0) | (alt ? 8 : 0) - return `${key}:${mask}` -} - -function signatureFromEvent(event: KeyboardEvent) { - return signature(normalizeKey(event.key), event.ctrlKey, event.metaKey, event.shiftKey, event.altKey) -} - -function isAllowedEditableKeybind(id: string | undefined) { - if (!id) return false - return EDITABLE_KEYBIND_IDS.has(actionId(id)) -} - -export type KeybindConfig = string - -export interface Keybind { - key: string - ctrl: boolean - meta: boolean - shift: boolean - alt: boolean -} - -export interface CommandOption { - id: string - title: string - description?: string - category?: string - keybind?: KeybindConfig - slash?: string - suggested?: boolean - disabled?: boolean - onSelect?: (source?: "palette" | "keybind" | "slash") => void - onHighlight?: () => (() => void) | void -} - -type CommandSource = "palette" | "keybind" | "slash" - -export type CommandCatalogItem = { - title: string - description?: string - category?: string - keybind?: KeybindConfig - slash?: string -} - -export type CommandRegistration = { - key?: string - options: Accessor -} - -export function upsertCommandRegistration(registrations: CommandRegistration[], entry: CommandRegistration) { - if (entry.key === undefined) return [entry, ...registrations] - return [entry, ...registrations.filter((x) => x.key !== entry.key)] -} - -export function parseKeybind(config: string): Keybind[] { - if (!config || config === "none") return [] - - return config.split(",").map((combo) => { - const parts = combo.trim().toLowerCase().split("+") - const keybind: Keybind = { - key: "", - ctrl: false, - meta: false, - shift: false, - alt: false, - } - - for (const part of parts) { - switch (part) { - case "ctrl": - case "control": - keybind.ctrl = true - break - case "meta": - case "cmd": - case "command": - keybind.meta = true - break - case "mod": - if (IS_MAC) keybind.meta = true - else keybind.ctrl = true - break - case "alt": - case "option": - keybind.alt = true - break - case "shift": - keybind.shift = true - break - default: - keybind.key = part - break - } - } - - return keybind - }) -} - -export function matchKeybind(keybinds: Keybind[], event: KeyboardEvent): boolean { - const eventKey = normalizeKey(event.key) - - for (const kb of keybinds) { - const keyMatch = kb.key === eventKey - const ctrlMatch = kb.ctrl === (event.ctrlKey || false) - const metaMatch = kb.meta === (event.metaKey || false) - const shiftMatch = kb.shift === (event.shiftKey || false) - const altMatch = kb.alt === (event.altKey || false) - - if (keyMatch && ctrlMatch && metaMatch && shiftMatch && altMatch) { - return true - } - } - - return false -} - -export function formatKeybind(config: string, t?: (key: KeyLabel) => string): string { - if (!config || config === "none") return "" - - const keybinds = parseKeybind(config) - if (keybinds.length === 0) return "" - - const kb = keybinds[0] - const parts: string[] = [] - - if (kb.ctrl) parts.push(IS_MAC ? "⌃" : keyText("common.key.ctrl", t)) - if (kb.alt) parts.push(IS_MAC ? "⌥" : keyText("common.key.alt", t)) - if (kb.shift) parts.push(IS_MAC ? "⇧" : keyText("common.key.shift", t)) - if (kb.meta) parts.push(IS_MAC ? "⌘" : keyText("common.key.meta", t)) - - if (kb.key) { - const keys: Record = { - arrowup: "↑", - arrowdown: "↓", - arrowleft: "←", - arrowright: "→", - comma: ",", - plus: "+", - } - const named: Record = { - backspace: "common.key.backspace", - delete: "common.key.delete", - end: "common.key.end", - enter: "common.key.enter", - esc: "common.key.esc", - escape: "common.key.esc", - home: "common.key.home", - insert: "common.key.insert", - pagedown: "common.key.pageDown", - pageup: "common.key.pageUp", - space: "common.key.space", - tab: "common.key.tab", - } - const key = kb.key.toLowerCase() - const displayKey = - keys[key] ?? - (named[key] - ? keyText(named[key], t) - : key.length === 1 - ? key.toUpperCase() - : key.charAt(0).toUpperCase() + key.slice(1)) - parts.push(displayKey) - } - - return IS_MAC ? parts.join("") : parts.join("+") -} - -function isEditableTarget(target: EventTarget | null) { - if (!(target instanceof HTMLElement)) return false - if (target.isContentEditable) return true - if (target.closest("[contenteditable='true']")) return true - if (target.closest("input, textarea, select")) return true - return false -} - -export const { use: useCommand, provider: CommandProvider } = createSimpleContext({ - name: "Command", - init: () => { - const dialog = useDialog() - const settings = useSettings() - const language = useLanguage() - const [store, setStore] = createStore({ - registrations: [] as CommandRegistration[], - suspendCount: 0, - }) - const warnedDuplicates = new Set() - - type CommandCatalog = Record - const [catalog, setCatalog, _, catalogReady] = persisted( - Persist.global("command.catalog.v1"), - createStore({}), - ) - - const bind = (id: string, def: KeybindConfig | undefined) => { - const custom = settings.keybinds.get(actionId(id)) - const config = custom ?? def - if (!config || config === "none") return - return config - } - - const registered = createMemo(() => { - const seen = new Set() - const all: CommandOption[] = [] - - for (const reg of store.registrations) { - for (const opt of reg.options()) { - if (seen.has(opt.id)) { - if (import.meta.env.DEV && !warnedDuplicates.has(opt.id)) { - warnedDuplicates.add(opt.id) - console.warn(`[command] duplicate command id "${opt.id}" registered; keeping first entry`) - } - continue - } - seen.add(opt.id) - all.push(opt) - } - } - - return all - }) - - createEffect(() => { - if (!catalogReady()) return - - setCatalog( - registered().reduce((acc, opt) => { - const id = actionId(opt.id) - acc[id] = { - title: opt.title, - description: opt.description, - category: opt.category, - keybind: opt.keybind, - slash: opt.slash, - } - return acc - }, {} as CommandCatalog), - ) - }) - - const catalogOptions = createMemo(() => Object.entries(catalog).map(([id, meta]) => ({ id, ...meta }))) - - const options = createMemo(() => { - const resolved = registered().map((opt) => ({ - ...opt, - keybind: bind(opt.id, opt.keybind), - })) - - const suggested = resolved.filter((x) => x.suggested && !x.disabled) - - return [ - ...suggested.map((x) => ({ - ...x, - id: SUGGESTED_PREFIX + x.id, - category: language.t("command.category.suggested"), - })), - ...resolved, - ] - }) - - const suspended = () => store.suspendCount > 0 - - const palette = createMemo(() => { - const config = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND - const keybinds = parseKeybind(config) - return new Set(keybinds.map((kb) => signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt))) - }) - - const keymap = createMemo(() => { - const map = new Map() - for (const option of options()) { - if (option.id.startsWith(SUGGESTED_PREFIX)) continue - if (option.disabled) continue - if (!option.keybind) continue - - const keybinds = parseKeybind(option.keybind) - for (const kb of keybinds) { - if (!kb.key) continue - const sig = signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt) - if (map.has(sig)) continue - map.set(sig, option) - } - } - return map - }) - - const optionMap = createMemo(() => { - const map = new Map() - for (const option of options()) { - map.set(option.id, option) - map.set(actionId(option.id), option) - } - return map - }) - - const run = (id: string, source?: CommandSource) => { - const option = optionMap().get(id) - option?.onSelect?.(source) - } - - const showPalette = () => { - run("file.open", "palette") - } - - const handleKeyDown = (event: KeyboardEvent) => { - if (suspended() || dialog.active) return - - const sig = signatureFromEvent(event) - const isPalette = palette().has(sig) - const option = keymap().get(sig) - const modified = event.ctrlKey || event.metaKey || event.altKey - const isTab = event.key === "Tab" - - if (isEditableTarget(event.target) && !isPalette && !isAllowedEditableKeybind(option?.id) && !modified && !isTab) - return - - if (isPalette) { - event.preventDefault() - showPalette() - return - } - - if (!option) return - event.preventDefault() - option.onSelect?.("keybind") - } - - onMount(() => { - makeEventListener(document, "keydown", handleKeyDown) - }) - - function register(cb: () => CommandOption[]): void - function register(key: string, cb: () => CommandOption[]): void - function register(key: string | (() => CommandOption[]), cb?: () => CommandOption[]) { - const id = typeof key === "string" ? key : undefined - const next = typeof key === "function" ? key : cb - if (!next) return - const options = createMemo(next) - const entry: CommandRegistration = { - key: id, - options, - } - setStore("registrations", (arr) => upsertCommandRegistration(arr, entry)) - onCleanup(() => { - setStore("registrations", (arr) => arr.filter((x) => x !== entry)) - }) - } - - return { - register, - trigger(id: string, source?: CommandSource) { - run(id, source) - }, - keybind(id: string) { - if (id === PALETTE_ID) { - return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND, language.t) - } - - const base = actionId(id) - const option = options().find((x) => actionId(x.id) === base) - if (option?.keybind) return formatKeybind(option.keybind, language.t) - - const meta = catalog[base] - const config = bind(base, meta?.keybind) - if (!config) return "" - return formatKeybind(config, language.t) - }, - show: showPalette, - keybinds(enabled: boolean) { - setStore("suspendCount", (count) => Math.max(0, count + (enabled ? -1 : 1))) - }, - suspended, - get catalog() { - return catalogOptions() - }, - get options() { - return options() - }, - } - }, -}) diff --git a/packages/app/src/context/comments.test.ts b/packages/app/src/context/comments.test.ts deleted file mode 100644 index 82fa170f2fc..00000000000 --- a/packages/app/src/context/comments.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { beforeAll, describe, expect, mock, test } from "bun:test" -import { createRoot } from "solid-js" -import type { LineComment } from "./comments" - -let createCommentSessionForTest: typeof import("./comments").createCommentSessionForTest - -beforeAll(async () => { - mock.module("@solidjs/router", () => ({ - useNavigate: () => () => undefined, - useParams: () => ({}), - })) - mock.module("@opencode-ai/ui/context", () => ({ - createSimpleContext: () => ({ - use: () => undefined, - provider: () => undefined, - }), - })) - const mod = await import("./comments") - createCommentSessionForTest = mod.createCommentSessionForTest -}) - -function line(file: string, id: string, time: number): LineComment { - return { - id, - file, - comment: id, - time, - selection: { start: 1, end: 1 }, - } -} - -describe("comments session indexing", () => { - test("keeps file list behavior and aggregate chronological order", () => { - createRoot((dispose) => { - const now = Date.now() - const comments = createCommentSessionForTest({ - "a.ts": [line("a.ts", "a-late", now + 20_000), line("a.ts", "a-early", now + 1_000)], - "b.ts": [line("b.ts", "b-mid", now + 10_000)], - }) - - expect(comments.list("a.ts").map((item) => item.id)).toEqual(["a-late", "a-early"]) - expect(comments.all().map((item) => item.id)).toEqual(["a-early", "b-mid", "a-late"]) - - const next = comments.add({ - file: "b.ts", - comment: "next", - selection: { start: 2, end: 2 }, - }) - - expect(comments.list("b.ts").at(-1)?.id).toBe(next.id) - expect(comments.all().map((item) => item.time)).toEqual( - comments - .all() - .map((item) => item.time) - .slice() - .sort((a, b) => a - b), - ) - - dispose() - }) - }) - - test("remove updates file and aggregate indexes consistently", () => { - createRoot((dispose) => { - const comments = createCommentSessionForTest({ - "a.ts": [line("a.ts", "a1", 10), line("a.ts", "shared", 20)], - "b.ts": [line("b.ts", "shared", 30)], - }) - - comments.setFocus({ file: "a.ts", id: "shared" }) - comments.setActive({ file: "a.ts", id: "shared" }) - comments.remove("a.ts", "shared") - - expect(comments.list("a.ts").map((item) => item.id)).toEqual(["a1"]) - expect( - comments - .all() - .filter((item) => item.id === "shared") - .map((item) => item.file), - ).toEqual(["b.ts"]) - expect(comments.focus()).toBeNull() - expect(comments.active()).toEqual({ file: "a.ts", id: "shared" }) - - dispose() - }) - }) - - test("clear resets file and aggregate indexes plus focus state", () => { - createRoot((dispose) => { - const comments = createCommentSessionForTest({ - "a.ts": [line("a.ts", "a1", 10)], - }) - - const next = comments.add({ - file: "b.ts", - comment: "next", - selection: { start: 2, end: 2 }, - }) - - comments.setActive({ file: "b.ts", id: next.id }) - comments.clear() - - expect(comments.list("a.ts")).toEqual([]) - expect(comments.list("b.ts")).toEqual([]) - expect(comments.all()).toEqual([]) - expect(comments.focus()).toBeNull() - expect(comments.active()).toBeNull() - - dispose() - }) - }) - - test("remove keeps focus when same comment id exists in another file", () => { - createRoot((dispose) => { - const comments = createCommentSessionForTest({ - "a.ts": [line("a.ts", "shared", 10)], - "b.ts": [line("b.ts", "shared", 20)], - }) - - comments.setFocus({ file: "b.ts", id: "shared" }) - comments.remove("a.ts", "shared") - - expect(comments.focus()).toEqual({ file: "b.ts", id: "shared" }) - expect(comments.list("a.ts")).toEqual([]) - expect(comments.list("b.ts").map((item) => item.id)).toEqual(["shared"]) - - dispose() - }) - }) - - test("setFocus and setActive updater callbacks receive current state", () => { - createRoot((dispose) => { - const comments = createCommentSessionForTest() - - comments.setFocus({ file: "a.ts", id: "a1" }) - comments.setFocus((current) => { - expect(current).toEqual({ file: "a.ts", id: "a1" }) - return { file: "b.ts", id: "b1" } - }) - - comments.setActive({ file: "c.ts", id: "c1" }) - comments.setActive((current) => { - expect(current).toEqual({ file: "c.ts", id: "c1" }) - return null - }) - - expect(comments.focus()).toEqual({ file: "b.ts", id: "b1" }) - expect(comments.active()).toBeNull() - - dispose() - }) - }) - - test("update changes only the targeted comment body", () => { - createRoot((dispose) => { - const comments = createCommentSessionForTest({ - "a.ts": [line("a.ts", "a1", 10), line("a.ts", "a2", 20)], - }) - - comments.update("a.ts", "a2", "edited") - - expect(comments.list("a.ts").map((item) => item.comment)).toEqual(["a1", "edited"]) - - dispose() - }) - }) - - test("replace swaps comment state and clears focus state", () => { - createRoot((dispose) => { - const comments = createCommentSessionForTest({ - "a.ts": [line("a.ts", "a1", 10)], - }) - - comments.setFocus({ file: "a.ts", id: "a1" }) - comments.setActive({ file: "a.ts", id: "a1" }) - comments.replace([line("b.ts", "b1", 30)]) - - expect(comments.list("a.ts")).toEqual([]) - expect(comments.list("b.ts").map((item) => item.id)).toEqual(["b1"]) - expect(comments.focus()).toBeNull() - expect(comments.active()).toBeNull() - - dispose() - }) - }) -}) diff --git a/packages/app/src/context/comments.tsx b/packages/app/src/context/comments.tsx deleted file mode 100644 index a97010c0af3..00000000000 --- a/packages/app/src/context/comments.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import { batch, createMemo, createRoot, onCleanup } from "solid-js" -import { createStore, reconcile, type SetStoreFunction, type Store } from "solid-js/store" -import { createSimpleContext } from "@opencode-ai/ui/context" -import { useParams } from "@solidjs/router" -import { Persist, persisted } from "@/utils/persist" -import { createScopedCache } from "@/utils/scoped-cache" -import { uuid } from "@/utils/uuid" -import type { SelectedLineRange } from "@/context/file" - -export type LineComment = { - id: string - file: string - selection: SelectedLineRange - comment: string - time: number -} - -type CommentFocus = { file: string; id: string } - -const WORKSPACE_KEY = "__workspace__" -const MAX_COMMENT_SESSIONS = 20 - -function sessionKey(dir: string, id: string | undefined) { - return `${dir}\n${id ?? WORKSPACE_KEY}` -} - -function decodeSessionKey(key: string) { - const split = key.lastIndexOf("\n") - if (split < 0) return { dir: key, id: WORKSPACE_KEY } - return { - dir: key.slice(0, split), - id: key.slice(split + 1), - } -} - -type CommentStore = { - comments: Record -} - -function aggregate(comments: Record) { - return Object.keys(comments) - .flatMap((file) => comments[file] ?? []) - .slice() - .sort((a, b) => a.time - b.time) -} - -function cloneSelection(selection: SelectedLineRange): SelectedLineRange { - const next: SelectedLineRange = { - start: selection.start, - end: selection.end, - } - - if (selection.side) next.side = selection.side - if (selection.endSide) next.endSide = selection.endSide - return next -} - -function cloneComment(comment: LineComment): LineComment { - return { - ...comment, - selection: cloneSelection(comment.selection), - } -} - -function group(comments: LineComment[]) { - return comments.reduce>((acc, comment) => { - const list = acc[comment.file] - const next = cloneComment(comment) - if (list) { - list.push(next) - return acc - } - acc[comment.file] = [next] - return acc - }, {}) -} - -function createCommentSessionState(store: Store, setStore: SetStoreFunction) { - const [state, setState] = createStore({ - focus: null as CommentFocus | null, - active: null as CommentFocus | null, - }) - - const all = () => aggregate(store.comments) - - const setRef = ( - key: "focus" | "active", - value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null), - ) => setState(key, value) - - const setFocus = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) => - setRef("focus", value) - - const setActive = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) => - setRef("active", value) - - const list = (file: string) => store.comments[file] ?? [] - - const add = (input: Omit) => { - const next: LineComment = { - id: uuid(), - time: Date.now(), - ...input, - selection: cloneSelection(input.selection), - } - - batch(() => { - setStore("comments", input.file, (items) => [...(items ?? []), next]) - setFocus({ file: input.file, id: next.id }) - }) - - return next - } - - const remove = (file: string, id: string) => { - batch(() => { - setStore("comments", file, (items) => (items ?? []).filter((item) => item.id !== id)) - setFocus((current) => (current?.file === file && current.id === id ? null : current)) - }) - } - - const update = (file: string, id: string, comment: string) => { - setStore("comments", file, (items) => - (items ?? []).map((item) => { - if (item.id !== id) return item - return { ...item, comment } - }), - ) - } - - const replace = (comments: LineComment[]) => { - batch(() => { - setStore("comments", reconcile(group(comments))) - setFocus(null) - setActive(null) - }) - } - - const clear = () => { - batch(() => { - setStore("comments", reconcile({})) - setFocus(null) - setActive(null) - }) - } - - return { - list, - all, - add, - remove, - update, - replace, - clear, - focus: () => state.focus, - setFocus, - clearFocus: () => setRef("focus", null), - active: () => state.active, - setActive, - clearActive: () => setRef("active", null), - } -} - -export function createCommentSessionForTest(comments: Record = {}) { - const [store, setStore] = createStore({ comments }) - return createCommentSessionState(store, setStore) -} - -function createCommentSession(dir: string, id: string | undefined) { - const legacy = `${dir}/comments${id ? "/" + id : ""}.v1` - - const [store, setStore, _, ready] = persisted( - Persist.scoped(dir, id, "comments", [legacy]), - createStore({ - comments: {}, - }), - ) - const session = createCommentSessionState(store, setStore) - - return { - ready, - list: session.list, - all: session.all, - add: session.add, - remove: session.remove, - update: session.update, - replace: session.replace, - clear: session.clear, - focus: session.focus, - setFocus: session.setFocus, - clearFocus: session.clearFocus, - active: session.active, - setActive: session.setActive, - clearActive: session.clearActive, - } -} - -export const { use: useComments, provider: CommentsProvider } = createSimpleContext({ - name: "Comments", - gate: false, - init: () => { - const params = useParams() - const cache = createScopedCache( - (key) => { - const decoded = decodeSessionKey(key) - return createRoot((dispose) => ({ - value: createCommentSession(decoded.dir, decoded.id === WORKSPACE_KEY ? undefined : decoded.id), - dispose, - })) - }, - { - maxEntries: MAX_COMMENT_SESSIONS, - dispose: (entry) => entry.dispose(), - }, - ) - - onCleanup(() => cache.clear()) - - const load = (dir: string, id: string | undefined) => { - const key = sessionKey(dir, id) - return cache.get(key).value - } - - const session = createMemo(() => load(params.dir!, params.id)) - - return { - ready: () => session().ready(), - list: (file: string) => session().list(file), - all: () => session().all(), - add: (input: Omit) => session().add(input), - remove: (file: string, id: string) => session().remove(file, id), - update: (file: string, id: string, comment: string) => session().update(file, id, comment), - replace: (comments: LineComment[]) => session().replace(comments), - clear: () => session().clear(), - focus: () => session().focus(), - setFocus: (focus: CommentFocus | null) => session().setFocus(focus), - clearFocus: () => session().clearFocus(), - active: () => session().active(), - setActive: (active: CommentFocus | null) => session().setActive(active), - clearActive: () => session().clearActive(), - } - }, -}) diff --git a/packages/app/src/context/file-content-eviction-accounting.test.ts b/packages/app/src/context/file-content-eviction-accounting.test.ts deleted file mode 100644 index 4ef5f947c7d..00000000000 --- a/packages/app/src/context/file-content-eviction-accounting.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test" -import { - evictContentLru, - getFileContentBytesTotal, - getFileContentEntryCount, - removeFileContentBytes, - resetFileContentLru, - setFileContentBytes, - touchFileContent, -} from "./file/content-cache" - -describe("file content eviction accounting", () => { - afterEach(() => { - resetFileContentLru() - }) - - test("updates byte totals incrementally for set, overwrite, remove, and reset", () => { - setFileContentBytes("a", 10) - setFileContentBytes("b", 15) - expect(getFileContentBytesTotal()).toBe(25) - expect(getFileContentEntryCount()).toBe(2) - - setFileContentBytes("a", 5) - expect(getFileContentBytesTotal()).toBe(20) - expect(getFileContentEntryCount()).toBe(2) - - touchFileContent("a") - expect(getFileContentBytesTotal()).toBe(20) - - removeFileContentBytes("b") - expect(getFileContentBytesTotal()).toBe(5) - expect(getFileContentEntryCount()).toBe(1) - - resetFileContentLru() - expect(getFileContentBytesTotal()).toBe(0) - expect(getFileContentEntryCount()).toBe(0) - }) - - test("evicts by entry cap using LRU order", () => { - for (const i of Array.from({ length: 41 }, (_, n) => n)) { - setFileContentBytes(`f-${i}`, 1) - } - - const evicted: string[] = [] - evictContentLru(undefined, (path) => evicted.push(path)) - - expect(evicted).toEqual(["f-0"]) - expect(getFileContentEntryCount()).toBe(40) - expect(getFileContentBytesTotal()).toBe(40) - }) - - test("evicts by byte cap while preserving protected entries", () => { - const chunk = 8 * 1024 * 1024 - setFileContentBytes("a", chunk) - setFileContentBytes("b", chunk) - setFileContentBytes("c", chunk) - - const evicted: string[] = [] - evictContentLru(new Set(["a"]), (path) => evicted.push(path)) - - expect(evicted).toEqual(["b"]) - expect(getFileContentEntryCount()).toBe(2) - expect(getFileContentBytesTotal()).toBe(chunk * 2) - }) -}) diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx deleted file mode 100644 index 8998731a6cb..00000000000 --- a/packages/app/src/context/file.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import { batch, createEffect, createMemo, onCleanup } from "solid-js" -import { createStore, produce, reconcile } from "solid-js/store" -import { createSimpleContext } from "@opencode-ai/ui/context" -import { showToast } from "@opencode-ai/ui/toast" -import { useParams } from "@solidjs/router" -import { getFilename } from "@opencode-ai/shared/util/path" -import { useSDK } from "./sdk" -import { useSync } from "./sync" -import { useLanguage } from "@/context/language" -import { useLayout } from "@/context/layout" -import { createPathHelpers } from "./file/path" -import { - approxBytes, - evictContentLru, - getFileContentBytesTotal, - getFileContentEntryCount, - hasFileContent, - removeFileContentBytes, - resetFileContentLru, - setFileContentBytes, - touchFileContent, -} from "./file/content-cache" -import { createFileViewCache } from "./file/view-cache" -import { createFileTreeStore } from "./file/tree-store" -import { invalidateFromWatcher } from "./file/watcher" -import { - selectionFromLines, - type FileState, - type FileSelection, - type FileViewState, - type SelectedLineRange, -} from "./file/types" - -export type { FileSelection, SelectedLineRange, FileViewState, FileState } -export { selectionFromLines } -export { - evictContentLru, - getFileContentBytesTotal, - getFileContentEntryCount, - removeFileContentBytes, - resetFileContentLru, - setFileContentBytes, - touchFileContent, -} - -function errorMessage(error: unknown, fallback: string) { - if (error instanceof Error && error.message) return error.message - if (typeof error === "string" && error) return error - return fallback -} - -export const { use: useFile, provider: FileProvider } = createSimpleContext({ - name: "File", - gate: false, - init: () => { - const sdk = useSDK() - useSync() - const params = useParams() - const language = useLanguage() - const layout = useLayout() - - const scope = createMemo(() => sdk.directory) - const path = createPathHelpers(scope) - const tabs = layout.tabs(() => `${params.dir}${params.id ? "/" + params.id : ""}`) - - const inflight = new Map>() - const [store, setStore] = createStore<{ - file: Record - }>({ - file: {}, - }) - - const tree = createFileTreeStore({ - scope, - normalizeDir: path.normalizeDir, - list: (dir) => sdk.client.file.list({ path: dir }).then((x) => x.data ?? []), - onError: (message) => { - showToast({ - variant: "error", - title: language.t("toast.file.listFailed.title"), - description: message, - }) - }, - }) - - const evictContent = (keep?: Set) => { - evictContentLru(keep, (target) => { - if (!store.file[target]) return - setStore( - "file", - target, - produce((draft) => { - draft.content = undefined - draft.loaded = false - }), - ) - }) - } - - createEffect(() => { - scope() - inflight.clear() - resetFileContentLru() - batch(() => { - setStore("file", reconcile({})) - tree.reset() - }) - }) - - const viewCache = createFileViewCache() - const view = createMemo(() => viewCache.load(scope(), params.id)) - - const ensure = (file: string) => { - if (!file) return - if (store.file[file]) return - setStore("file", file, { path: file, name: getFilename(file) }) - } - - const setLoading = (file: string) => { - setStore( - "file", - file, - produce((draft) => { - draft.loading = true - draft.error = undefined - }), - ) - } - - const setLoaded = (file: string, content: FileState["content"]) => { - setStore( - "file", - file, - produce((draft) => { - draft.loaded = true - draft.loading = false - draft.content = content - }), - ) - } - - const setLoadError = (file: string, message: string) => { - setStore( - "file", - file, - produce((draft) => { - draft.loading = false - draft.error = message - }), - ) - showToast({ - variant: "error", - title: language.t("toast.file.loadFailed.title"), - description: message, - }) - } - - const load = (input: string, options?: { force?: boolean }) => { - const file = path.normalize(input) - if (!file) return Promise.resolve() - - const directory = scope() - const key = `${directory}\n${file}` - ensure(file) - - const current = store.file[file] - if (!options?.force && current?.loaded) return Promise.resolve() - - const pending = inflight.get(key) - if (pending) return pending - - setLoading(file) - - const promise = sdk.client.file - .read({ path: file }) - .then((x) => { - if (scope() !== directory) return - const content = x.data - setLoaded(file, content) - - if (!content) return - touchFileContent(file, approxBytes(content)) - evictContent(new Set([file])) - }) - .catch((e) => { - if (scope() !== directory) return - setLoadError(file, errorMessage(e, language.t("error.chain.unknown"))) - }) - .finally(() => { - inflight.delete(key) - }) - - inflight.set(key, promise) - return promise - } - - const search = (query: string, dirs: "true" | "false") => - sdk.client.find.files({ query, dirs }).then( - (x) => (x.data ?? []).map(path.normalize), - () => [], - ) - - const stop = sdk.event.listen((e) => { - invalidateFromWatcher(e.details, { - normalize: path.normalize, - hasFile: (file) => Boolean(store.file[file]), - isOpen: (file) => tabs.all().some((tab) => path.pathFromTab(tab) === file), - loadFile: (file) => { - void load(file, { force: true }) - }, - node: tree.node, - isDirLoaded: tree.isLoaded, - refreshDir: (dir) => { - void tree.listDir(dir, { force: true }) - }, - }) - }) - - const get = (input: string) => { - const file = path.normalize(input) - const state = store.file[file] - const content = state?.content - if (!content) return state - if (hasFileContent(file)) { - touchFileContent(file) - return state - } - touchFileContent(file, approxBytes(content)) - return state - } - - function withPath(input: string, action: (file: string) => unknown) { - return action(path.normalize(input)) - } - const scrollTop = (input: string) => withPath(input, (file) => view().scrollTop(file)) - const scrollLeft = (input: string) => withPath(input, (file) => view().scrollLeft(file)) - const selectedLines = (input: string) => withPath(input, (file) => view().selectedLines(file)) - const setScrollTop = (input: string, top: number) => withPath(input, (file) => view().setScrollTop(file, top)) - const setScrollLeft = (input: string, left: number) => withPath(input, (file) => view().setScrollLeft(file, left)) - const setSelectedLines = (input: string, range: SelectedLineRange | null) => - withPath(input, (file) => view().setSelectedLines(file, range)) - - onCleanup(() => { - stop() - viewCache.clear() - }) - - return { - ready: () => view().ready(), - normalize: path.normalize, - tab: path.tab, - pathFromTab: path.pathFromTab, - tree: { - list: tree.listDir, - refresh: (input: string) => tree.listDir(input, { force: true }), - state: tree.dirState, - children: tree.children, - expand: tree.expandDir, - collapse: tree.collapseDir, - toggle(input: string) { - if (tree.dirState(input)?.expanded) { - tree.collapseDir(input) - return - } - tree.expandDir(input) - }, - }, - get, - load, - scrollTop, - scrollLeft, - setScrollTop, - setScrollLeft, - selectedLines, - setSelectedLines, - searchFiles: (query: string) => search(query, "false"), - searchFilesAndDirectories: (query: string) => search(query, "true"), - } - }, -}) diff --git a/packages/app/src/context/file/content-cache.ts b/packages/app/src/context/file/content-cache.ts deleted file mode 100644 index 98de196ab6f..00000000000 --- a/packages/app/src/context/file/content-cache.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { FileContent } from "@kilocode/sdk/v2" - -const MAX_FILE_CONTENT_ENTRIES = 40 -const MAX_FILE_CONTENT_BYTES = 20 * 1024 * 1024 - -const lru = new Map() -let total = 0 - -export function approxBytes(content: FileContent) { - const patchBytes = - content.patch?.hunks.reduce((sum, hunk) => { - return sum + hunk.lines.reduce((lineSum, line) => lineSum + line.length, 0) - }, 0) ?? 0 - - return (content.content.length + (content.diff?.length ?? 0) + patchBytes) * 2 -} - -function setBytes(path: string, nextBytes: number) { - const prev = lru.get(path) - if (prev !== undefined) total -= prev - lru.delete(path) - lru.set(path, nextBytes) - total += nextBytes -} - -function touch(path: string, bytes?: number) { - const prev = lru.get(path) - if (prev === undefined && bytes === undefined) return - setBytes(path, bytes ?? prev ?? 0) -} - -function remove(path: string) { - const prev = lru.get(path) - if (prev === undefined) return - lru.delete(path) - total -= prev -} - -function reset() { - lru.clear() - total = 0 -} - -export function evictContentLru(keep: Set | undefined, evict: (path: string) => void) { - const set = keep ?? new Set() - - while (lru.size > MAX_FILE_CONTENT_ENTRIES || total > MAX_FILE_CONTENT_BYTES) { - const path = lru.keys().next().value - if (!path) return - - if (set.has(path)) { - touch(path) - if (lru.size <= set.size) return - continue - } - - remove(path) - evict(path) - } -} - -export function resetFileContentLru() { - reset() -} - -export function setFileContentBytes(path: string, bytes: number) { - setBytes(path, bytes) -} - -export function removeFileContentBytes(path: string) { - remove(path) -} - -export function touchFileContent(path: string, bytes?: number) { - touch(path, bytes) -} - -export function getFileContentBytesTotal() { - return total -} - -export function getFileContentEntryCount() { - return lru.size -} - -export function hasFileContent(path: string) { - return lru.has(path) -} diff --git a/packages/app/src/context/file/path.test.ts b/packages/app/src/context/file/path.test.ts deleted file mode 100644 index feef6d466ef..00000000000 --- a/packages/app/src/context/file/path.test.ts +++ /dev/null @@ -1,360 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { createPathHelpers, stripQueryAndHash, unquoteGitPath, encodeFilePath } from "./path" - -describe("file path helpers", () => { - test("normalizes file inputs against workspace root", () => { - const path = createPathHelpers(() => "/repo") - expect(path.normalize("file:///repo/src/app.ts?x=1#h")).toBe("src/app.ts") - expect(path.normalize("/repo/src/app.ts")).toBe("src/app.ts") - expect(path.normalize("./src/app.ts")).toBe("src/app.ts") - expect(path.normalizeDir("src/components///")).toBe("src/components") - expect(path.tab("src/app.ts")).toBe("file://src/app.ts") - expect(path.pathFromTab("file://src/app.ts")).toBe("src/app.ts") - expect(path.pathFromTab("other://src/app.ts")).toBeUndefined() - }) - - test("normalizes Windows absolute paths with mixed separators", () => { - const path = createPathHelpers(() => "C:\\repo") - expect(path.normalize("C:\\repo\\src\\app.ts")).toBe("src\\app.ts") - expect(path.normalize("C:/repo/src/app.ts")).toBe("src/app.ts") - expect(path.normalize("file://C:/repo/src/app.ts")).toBe("src/app.ts") - expect(path.normalize("c:\\repo\\src\\app.ts")).toBe("src\\app.ts") - }) - - test("keeps query/hash stripping behavior stable", () => { - expect(stripQueryAndHash("a/b.ts#L12?x=1")).toBe("a/b.ts") - expect(stripQueryAndHash("a/b.ts?x=1#L12")).toBe("a/b.ts") - expect(stripQueryAndHash("a/b.ts")).toBe("a/b.ts") - }) - - test("unquotes git escaped octal path strings", () => { - expect(unquoteGitPath('"a/\\303\\251.txt"')).toBe("a/\u00e9.txt") - expect(unquoteGitPath('"plain\\nname"')).toBe("plain\nname") - expect(unquoteGitPath("a/b/c.ts")).toBe("a/b/c.ts") - }) -}) - -describe("encodeFilePath", () => { - describe("Linux/Unix paths", () => { - test("should handle Linux absolute path", () => { - const linuxPath = "/home/user/project/README.md" - const result = encodeFilePath(linuxPath) - const fileUrl = `file://${result}` - - // Should create a valid URL - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).toBe("/home/user/project/README.md") - - const url = new URL(fileUrl) - expect(url.protocol).toBe("file:") - expect(url.pathname).toBe("/home/user/project/README.md") - }) - - test("should handle Linux path with special characters", () => { - const linuxPath = "/home/user/file#name with spaces.txt" - const result = encodeFilePath(linuxPath) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).toBe("/home/user/file%23name%20with%20spaces.txt") - }) - - test("should handle Linux relative path", () => { - const relativePath = "src/components/App.tsx" - const result = encodeFilePath(relativePath) - - expect(result).toBe("src/components/App.tsx") - }) - - test("should handle Linux root directory", () => { - const result = encodeFilePath("/") - expect(result).toBe("/") - }) - - test("should handle Linux path with all special chars", () => { - const path = "/path/to/file#with?special%chars&more.txt" - const result = encodeFilePath(path) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).toContain("%23") // # - expect(result).toContain("%3F") // ? - expect(result).toContain("%25") // % - expect(result).toContain("%26") // & - }) - }) - - describe("macOS paths", () => { - test("should handle macOS absolute path", () => { - const macPath = "/Users/kelvin/Projects/opencode/README.md" - const result = encodeFilePath(macPath) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).toBe("/Users/kelvin/Projects/opencode/README.md") - }) - - test("should handle macOS path with spaces", () => { - const macPath = "/Users/kelvin/My Documents/file.txt" - const result = encodeFilePath(macPath) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).toContain("My%20Documents") - }) - }) - - describe("Windows paths", () => { - test("should handle Windows absolute path with backslashes", () => { - const windowsPath = "D:\\dev\\projects\\opencode\\README.bs.md" - const result = encodeFilePath(windowsPath) - const fileUrl = `file://${result}` - - // Should create a valid, parseable URL - expect(() => new URL(fileUrl)).not.toThrow() - - const url = new URL(fileUrl) - expect(url.protocol).toBe("file:") - expect(url.pathname).toContain("README.bs.md") - expect(result).toBe("/D:/dev/projects/opencode/README.bs.md") - }) - - test("should handle mixed separator path (Windows + Unix)", () => { - // This is what happens in build-request-parts.ts when concatenating paths - const mixedPath = "D:\\dev\\projects\\opencode/README.bs.md" - const result = encodeFilePath(mixedPath) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).toBe("/D:/dev/projects/opencode/README.bs.md") - }) - - test("should handle Windows path with spaces", () => { - const windowsPath = "C:\\Program Files\\MyApp\\file with spaces.txt" - const result = encodeFilePath(windowsPath) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).toContain("Program%20Files") - expect(result).toContain("file%20with%20spaces.txt") - }) - - test("should handle Windows path with special chars in filename", () => { - const windowsPath = "D:\\projects\\file#name with ?marks.txt" - const result = encodeFilePath(windowsPath) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).toContain("file%23name%20with%20%3Fmarks.txt") - }) - - test("should handle Windows root directory", () => { - const windowsPath = "C:\\" - const result = encodeFilePath(windowsPath) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).toBe("/C:/") - }) - - test("should handle Windows relative path with backslashes", () => { - const windowsPath = "src\\components\\App.tsx" - const result = encodeFilePath(windowsPath) - - // Relative paths shouldn't get the leading slash - expect(result).toBe("src/components/App.tsx") - }) - - test("should NOT create invalid URL like the bug report", () => { - // This is the exact scenario from bug report by @alexyaroshuk - const windowsPath = "D:\\dev\\projects\\opencode\\README.bs.md" - const result = encodeFilePath(windowsPath) - const fileUrl = `file://${result}` - - // The bug was creating: file://D%3A%5Cdev%5Cprojects%5Copencode/README.bs.md - expect(result).not.toContain("%5C") // Should not have encoded backslashes - expect(result).not.toBe("D%3A%5Cdev%5Cprojects%5Copencode/README.bs.md") - - // Should be valid - expect(() => new URL(fileUrl)).not.toThrow() - }) - - test("should handle lowercase drive letters", () => { - const windowsPath = "c:\\users\\test\\file.txt" - const result = encodeFilePath(windowsPath) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).toBe("/c:/users/test/file.txt") - }) - }) - - describe("Cross-platform compatibility", () => { - test("should preserve Unix paths unchanged (except encoding)", () => { - const unixPath = "/usr/local/bin/app" - const result = encodeFilePath(unixPath) - expect(result).toBe("/usr/local/bin/app") - }) - - test("should normalize Windows paths for cross-platform use", () => { - const windowsPath = "C:\\Users\\test\\file.txt" - const result = encodeFilePath(windowsPath) - // Should convert to forward slashes and add leading / - expect(result).not.toContain("\\") - expect(result).toMatch(/^\/[A-Za-z]:\//) - }) - - test("should handle relative paths the same on all platforms", () => { - const unixRelative = "src/app.ts" - const windowsRelative = "src\\app.ts" - - const unixResult = encodeFilePath(unixRelative) - const windowsResult = encodeFilePath(windowsRelative) - - // Both should normalize to forward slashes - expect(unixResult).toBe("src/app.ts") - expect(windowsResult).toBe("src/app.ts") - }) - }) - - describe("Edge cases", () => { - test("should handle empty path", () => { - const result = encodeFilePath("") - expect(result).toBe("") - }) - - test("should handle path with multiple consecutive slashes", () => { - const result = encodeFilePath("//path//to///file.txt") - // Multiple slashes should be preserved (backend handles normalization) - expect(result).toBe("//path//to///file.txt") - }) - - test("should encode Unicode characters", () => { - const unicodePath = "/home/user/文档/README.md" - const result = encodeFilePath(unicodePath) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - // Unicode should be encoded - expect(result).toContain("%E6%96%87%E6%A1%A3") - }) - - test("should handle already normalized Windows path", () => { - // Path that's already been normalized (has / before drive letter) - const alreadyNormalized = "/D:/path/file.txt" - const result = encodeFilePath(alreadyNormalized) - - // Should not add another leading slash - expect(result).toBe("/D:/path/file.txt") - expect(result).not.toContain("//D") - }) - - test("should handle just drive letter", () => { - const justDrive = "D:" - const result = encodeFilePath(justDrive) - const fileUrl = `file://${result}` - - expect(result).toBe("/D:") - expect(() => new URL(fileUrl)).not.toThrow() - }) - - test("should handle Windows path with trailing backslash", () => { - const trailingBackslash = "C:\\Users\\test\\" - const result = encodeFilePath(trailingBackslash) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).toBe("/C:/Users/test/") - }) - - test("should handle very long paths", () => { - const longPath = "C:\\Users\\test\\" + "verylongdirectoryname\\".repeat(20) + "file.txt" - const result = encodeFilePath(longPath) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).not.toContain("\\") - }) - - test("should handle paths with dots", () => { - const pathWithDots = "C:\\Users\\..\\test\\.\\file.txt" - const result = encodeFilePath(pathWithDots) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - // Dots should be preserved (backend normalizes) - expect(result).toContain("..") - expect(result).toContain("/./") - }) - }) - - describe("Regression tests for PR #12424", () => { - test("should handle file with # in name", () => { - const path = "/path/to/file#name.txt" - const result = encodeFilePath(path) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).toBe("/path/to/file%23name.txt") - }) - - test("should handle file with ? in name", () => { - const path = "/path/to/file?name.txt" - const result = encodeFilePath(path) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).toBe("/path/to/file%3Fname.txt") - }) - - test("should handle file with % in name", () => { - const path = "/path/to/file%name.txt" - const result = encodeFilePath(path) - const fileUrl = `file://${result}` - - expect(() => new URL(fileUrl)).not.toThrow() - expect(result).toBe("/path/to/file%25name.txt") - }) - }) - - describe("Integration with file:// URL construction", () => { - test("should work with query parameters (Linux)", () => { - const path = "/home/user/file.txt" - const encoded = encodeFilePath(path) - const fileUrl = `file://${encoded}?start=10&end=20` - - const url = new URL(fileUrl) - expect(url.searchParams.get("start")).toBe("10") - expect(url.searchParams.get("end")).toBe("20") - expect(url.pathname).toBe("/home/user/file.txt") - }) - - test("should work with query parameters (Windows)", () => { - const path = "C:\\Users\\test\\file.txt" - const encoded = encodeFilePath(path) - const fileUrl = `file://${encoded}?start=10&end=20` - - const url = new URL(fileUrl) - expect(url.searchParams.get("start")).toBe("10") - expect(url.searchParams.get("end")).toBe("20") - }) - - test("should parse correctly in URL constructor (Linux)", () => { - const path = "/var/log/app.log" - const fileUrl = `file://${encodeFilePath(path)}` - const url = new URL(fileUrl) - - expect(url.protocol).toBe("file:") - expect(url.pathname).toBe("/var/log/app.log") - }) - - test("should parse correctly in URL constructor (Windows)", () => { - const path = "D:\\logs\\app.log" - const fileUrl = `file://${encodeFilePath(path)}` - const url = new URL(fileUrl) - - expect(url.protocol).toBe("file:") - expect(url.pathname).toContain("app.log") - }) - }) -}) diff --git a/packages/app/src/context/file/path.ts b/packages/app/src/context/file/path.ts deleted file mode 100644 index 53f072b6cb2..00000000000 --- a/packages/app/src/context/file/path.ts +++ /dev/null @@ -1,151 +0,0 @@ -export function stripFileProtocol(input: string) { - if (!input.startsWith("file://")) return input - return input.slice("file://".length) -} - -export function stripQueryAndHash(input: string) { - const hashIndex = input.indexOf("#") - const queryIndex = input.indexOf("?") - - if (hashIndex !== -1 && queryIndex !== -1) { - return input.slice(0, Math.min(hashIndex, queryIndex)) - } - - if (hashIndex !== -1) return input.slice(0, hashIndex) - if (queryIndex !== -1) return input.slice(0, queryIndex) - return input -} - -export function unquoteGitPath(input: string) { - if (!input.startsWith('"')) return input - if (!input.endsWith('"')) return input - const body = input.slice(1, -1) - const bytes: number[] = [] - - for (let i = 0; i < body.length; i++) { - const char = body[i]! - if (char !== "\\") { - bytes.push(char.charCodeAt(0)) - continue - } - - const next = body[i + 1] - if (!next) { - bytes.push("\\".charCodeAt(0)) - continue - } - - if (next >= "0" && next <= "7") { - const chunk = body.slice(i + 1, i + 4) - const match = chunk.match(/^[0-7]{1,3}/) - if (!match) { - bytes.push(next.charCodeAt(0)) - i++ - continue - } - bytes.push(parseInt(match[0], 8)) - i += match[0].length - continue - } - - const escaped = - next === "n" - ? "\n" - : next === "r" - ? "\r" - : next === "t" - ? "\t" - : next === "b" - ? "\b" - : next === "f" - ? "\f" - : next === "v" - ? "\v" - : next === "\\" || next === '"' - ? next - : undefined - - bytes.push((escaped ?? next).charCodeAt(0)) - i++ - } - - return new TextDecoder().decode(new Uint8Array(bytes)) -} - -export function decodeFilePath(input: string) { - try { - return decodeURIComponent(input) - } catch { - return input - } -} - -export function encodeFilePath(filepath: string): string { - // Normalize Windows paths: convert backslashes to forward slashes - let normalized = filepath.replace(/\\/g, "/") - - // Handle Windows absolute paths (D:/path -> /D:/path for proper file:// URLs) - if (/^[A-Za-z]:/.test(normalized)) { - normalized = "/" + normalized - } - - // Encode each path segment (preserving forward slashes as path separators) - // Keep the colon in Windows drive letters (`/C:/...`) so downstream file URL parsers - // can reliably detect drives. - return normalized - .split("/") - .map((segment, index) => { - if (index === 1 && /^[A-Za-z]:$/.test(segment)) return segment - return encodeURIComponent(segment) - }) - .join("/") -} - -export function createPathHelpers(scope: () => string) { - const normalize = (input: string) => { - const root = scope() - - let path = unquoteGitPath(decodeFilePath(stripQueryAndHash(stripFileProtocol(input)))) - - // Separator-agnostic prefix stripping for Cygwin/native Windows compatibility - // Only case-insensitive on Windows (drive letter or UNC paths) - const windows = /^[A-Za-z]:/.test(root) || root.startsWith("\\\\") - const canonRoot = windows ? root.replace(/\\/g, "/").toLowerCase() : root.replace(/\\/g, "/") - const canonPath = windows ? path.replace(/\\/g, "/").toLowerCase() : path.replace(/\\/g, "/") - if ( - canonPath.startsWith(canonRoot) && - (canonRoot.endsWith("/") || canonPath === canonRoot || canonPath[canonRoot.length] === "/") - ) { - // Slice from original path to preserve native separators - path = path.slice(root.length) - } - - if (path.startsWith("./") || path.startsWith(".\\")) { - path = path.slice(2) - } - - if (path.startsWith("/") || path.startsWith("\\")) { - path = path.slice(1) - } - return path - } - - const tab = (input: string) => { - const path = normalize(input) - return `file://${encodeFilePath(path)}` - } - - const pathFromTab = (tabValue: string) => { - if (!tabValue.startsWith("file://")) return - return normalize(tabValue) - } - - const normalizeDir = (input: string) => normalize(input).replace(/\/+$/, "") - - return { - normalize, - tab, - pathFromTab, - normalizeDir, - } -} diff --git a/packages/app/src/context/file/tree-store.ts b/packages/app/src/context/file/tree-store.ts deleted file mode 100644 index dc4e58f7a25..00000000000 --- a/packages/app/src/context/file/tree-store.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { createStore, produce, reconcile } from "solid-js/store" -import type { FileNode } from "@kilocode/sdk/v2" - -type DirectoryState = { - expanded: boolean - loaded?: boolean - loading?: boolean - error?: string - children?: string[] -} - -type TreeStoreOptions = { - scope: () => string - normalizeDir: (input: string) => string - list: (input: string) => Promise - onError: (message: string) => void -} - -export function createFileTreeStore(options: TreeStoreOptions) { - const [tree, setTree] = createStore<{ - node: Record - dir: Record - }>({ - node: {}, - dir: { "": { expanded: true } }, - }) - - const inflight = new Map>() - - const reset = () => { - inflight.clear() - setTree("node", reconcile({})) - setTree("dir", reconcile({})) - setTree("dir", "", { expanded: true }) - } - - const ensureDir = (path: string) => { - if (tree.dir[path]) return - setTree("dir", path, { expanded: false }) - } - - const listDir = (input: string, opts?: { force?: boolean }) => { - const dir = options.normalizeDir(input) - ensureDir(dir) - - const current = tree.dir[dir] - if (!opts?.force && current?.loaded) return Promise.resolve() - - const pending = inflight.get(dir) - if (pending) return pending - - setTree( - "dir", - dir, - produce((draft) => { - draft.loading = true - draft.error = undefined - }), - ) - - const directory = options.scope() - - const promise = options - .list(dir) - .then((nodes) => { - if (options.scope() !== directory) return - const prevChildren = tree.dir[dir]?.children ?? [] - const nextChildren = nodes.map((node) => node.path) - const nextSet = new Set(nextChildren) - - setTree( - "node", - produce((draft) => { - const removedDirs: string[] = [] - - for (const child of prevChildren) { - if (nextSet.has(child)) continue - const existing = draft[child] - if (existing?.type === "directory") removedDirs.push(child) - delete draft[child] - } - - if (removedDirs.length > 0) { - const keys = Object.keys(draft) - for (const key of keys) { - for (const removed of removedDirs) { - if (!key.startsWith(removed + "/")) continue - delete draft[key] - break - } - } - } - - for (const node of nodes) { - draft[node.path] = node - } - }), - ) - - setTree( - "dir", - dir, - produce((draft) => { - draft.loaded = true - draft.loading = false - draft.children = nextChildren - }), - ) - }) - .catch((e) => { - if (options.scope() !== directory) return - setTree( - "dir", - dir, - produce((draft) => { - draft.loading = false - draft.error = e.message - }), - ) - options.onError(e.message) - }) - .finally(() => { - inflight.delete(dir) - }) - - inflight.set(dir, promise) - return promise - } - - const expandDir = (input: string) => { - const dir = options.normalizeDir(input) - ensureDir(dir) - setTree("dir", dir, "expanded", true) - void listDir(dir) - } - - const collapseDir = (input: string) => { - const dir = options.normalizeDir(input) - ensureDir(dir) - setTree("dir", dir, "expanded", false) - } - - const dirState = (input: string) => { - const dir = options.normalizeDir(input) - return tree.dir[dir] - } - - const children = (input: string) => { - const dir = options.normalizeDir(input) - const ids = tree.dir[dir]?.children - if (!ids) return [] - const out: FileNode[] = [] - for (const id of ids) { - const node = tree.node[id] - if (node) out.push(node) - } - return out - } - - return { - listDir, - expandDir, - collapseDir, - dirState, - children, - node: (path: string) => tree.node[path], - isLoaded: (path: string) => Boolean(tree.dir[path]?.loaded), - reset, - } -} diff --git a/packages/app/src/context/file/types.ts b/packages/app/src/context/file/types.ts deleted file mode 100644 index ee1cc49f311..00000000000 --- a/packages/app/src/context/file/types.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { FileContent } from "@kilocode/sdk/v2" - -export type FileSelection = { - startLine: number - startChar: number - endLine: number - endChar: number -} - -export type SelectedLineRange = { - start: number - end: number - side?: "additions" | "deletions" - endSide?: "additions" | "deletions" -} - -export type FileViewState = { - scrollTop?: number - scrollLeft?: number - selectedLines?: SelectedLineRange | null -} - -export type FileState = { - path: string - name: string - loaded?: boolean - loading?: boolean - error?: string - content?: FileContent -} - -export function selectionFromLines(range: SelectedLineRange): FileSelection { - const startLine = Math.min(range.start, range.end) - const endLine = Math.max(range.start, range.end) - return { - startLine, - endLine, - startChar: 0, - endChar: 0, - } -} diff --git a/packages/app/src/context/file/view-cache.ts b/packages/app/src/context/file/view-cache.ts deleted file mode 100644 index 4c060174ab8..00000000000 --- a/packages/app/src/context/file/view-cache.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { createEffect, createRoot } from "solid-js" -import { createStore, produce } from "solid-js/store" -import { Persist, persisted } from "@/utils/persist" -import { createScopedCache } from "@/utils/scoped-cache" -import type { FileViewState, SelectedLineRange } from "./types" - -const WORKSPACE_KEY = "__workspace__" -const MAX_FILE_VIEW_SESSIONS = 20 -const MAX_VIEW_FILES = 500 - -function normalizeSelectedLines(range: SelectedLineRange): SelectedLineRange { - if (range.start <= range.end) return { ...range } - - const startSide = range.side - const endSide = range.endSide ?? startSide - - return { - ...range, - start: range.end, - end: range.start, - side: endSide, - endSide: startSide !== endSide ? startSide : undefined, - } -} - -function equalSelectedLines(a: SelectedLineRange | null | undefined, b: SelectedLineRange | null | undefined) { - if (!a && !b) return true - if (!a || !b) return false - const left = normalizeSelectedLines(a) - const right = normalizeSelectedLines(b) - return ( - left.start === right.start && left.end === right.end && left.side === right.side && left.endSide === right.endSide - ) -} - -function createViewSession(dir: string, id: string | undefined) { - const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1` - - const [view, setView, _, ready] = persisted( - Persist.scoped(dir, id, "file-view", [legacyViewKey]), - createStore<{ - file: Record - }>({ - file: {}, - }), - ) - - const meta = { pruned: false } - - const pruneView = (keep?: string) => { - const keys = Object.keys(view.file) - if (keys.length <= MAX_VIEW_FILES) return - - const drop = keys.filter((key) => key !== keep).slice(0, keys.length - MAX_VIEW_FILES) - if (drop.length === 0) return - - setView( - produce((draft) => { - for (const key of drop) { - delete draft.file[key] - } - }), - ) - } - - createEffect(() => { - if (!ready()) return - if (meta.pruned) return - meta.pruned = true - pruneView() - }) - - const scrollTop = (path: string) => view.file[path]?.scrollTop - const scrollLeft = (path: string) => view.file[path]?.scrollLeft - const selectedLines = (path: string) => view.file[path]?.selectedLines - - const setScrollTop = (path: string, top: number) => { - setView( - produce((draft) => { - const file = draft.file[path] ?? (draft.file[path] = {}) - if (file.scrollTop === top) return - file.scrollTop = top - }), - ) - pruneView(path) - } - - const setScrollLeft = (path: string, left: number) => { - setView( - produce((draft) => { - const file = draft.file[path] ?? (draft.file[path] = {}) - if (file.scrollLeft === left) return - file.scrollLeft = left - }), - ) - pruneView(path) - } - - const setSelectedLines = (path: string, range: SelectedLineRange | null) => { - const next = range ? normalizeSelectedLines(range) : null - setView( - produce((draft) => { - const file = draft.file[path] ?? (draft.file[path] = {}) - if (equalSelectedLines(file.selectedLines, next)) return - file.selectedLines = next - }), - ) - pruneView(path) - } - - return { - ready, - scrollTop, - scrollLeft, - selectedLines, - setScrollTop, - setScrollLeft, - setSelectedLines, - } -} - -export function createFileViewCache() { - const cache = createScopedCache( - (key) => { - const split = key.lastIndexOf("\n") - const dir = split >= 0 ? key.slice(0, split) : key - const id = split >= 0 ? key.slice(split + 1) : WORKSPACE_KEY - return createRoot((dispose) => ({ - value: createViewSession(dir, id === WORKSPACE_KEY ? undefined : id), - dispose, - })) - }, - { - maxEntries: MAX_FILE_VIEW_SESSIONS, - dispose: (entry) => entry.dispose(), - }, - ) - - return { - load: (dir: string, id: string | undefined) => { - const key = `${dir}\n${id ?? WORKSPACE_KEY}` - return cache.get(key).value - }, - clear: () => cache.clear(), - } -} diff --git a/packages/app/src/context/file/watcher.test.ts b/packages/app/src/context/file/watcher.test.ts deleted file mode 100644 index 9536b52536b..00000000000 --- a/packages/app/src/context/file/watcher.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { invalidateFromWatcher } from "./watcher" - -describe("file watcher invalidation", () => { - test("reloads open files and refreshes loaded parent on add", () => { - const loads: string[] = [] - const refresh: string[] = [] - invalidateFromWatcher( - { - type: "file.watcher.updated", - properties: { - file: "src/new.ts", - event: "add", - }, - }, - { - normalize: (input) => input, - hasFile: (path) => path === "src/new.ts", - loadFile: (path) => loads.push(path), - node: () => undefined, - isDirLoaded: (path) => path === "src", - refreshDir: (path) => refresh.push(path), - }, - ) - - expect(loads).toEqual(["src/new.ts"]) - expect(refresh).toEqual(["src"]) - }) - - test("reloads files that are open in tabs", () => { - const loads: string[] = [] - - invalidateFromWatcher( - { - type: "file.watcher.updated", - properties: { - file: "src/open.ts", - event: "change", - }, - }, - { - normalize: (input) => input, - hasFile: () => false, - isOpen: (path) => path === "src/open.ts", - loadFile: (path) => loads.push(path), - node: () => ({ - path: "src/open.ts", - type: "file", - name: "open.ts", - absolute: "/repo/src/open.ts", - ignored: false, - }), - isDirLoaded: () => false, - refreshDir: () => {}, - }, - ) - - expect(loads).toEqual(["src/open.ts"]) - }) - - test("refreshes only changed loaded directory nodes", () => { - const refresh: string[] = [] - - invalidateFromWatcher( - { - type: "file.watcher.updated", - properties: { - file: "src", - event: "change", - }, - }, - { - normalize: (input) => input, - hasFile: () => false, - loadFile: () => {}, - node: () => ({ path: "src", type: "directory", name: "src", absolute: "/repo/src", ignored: false }), - isDirLoaded: (path) => path === "src", - refreshDir: (path) => refresh.push(path), - }, - ) - - invalidateFromWatcher( - { - type: "file.watcher.updated", - properties: { - file: "src/file.ts", - event: "change", - }, - }, - { - normalize: (input) => input, - hasFile: () => false, - loadFile: () => {}, - node: () => ({ - path: "src/file.ts", - type: "file", - name: "file.ts", - absolute: "/repo/src/file.ts", - ignored: false, - }), - isDirLoaded: () => true, - refreshDir: (path) => refresh.push(path), - }, - ) - - expect(refresh).toEqual(["src"]) - }) - - test("ignores invalid or git watcher updates", () => { - const refresh: string[] = [] - - invalidateFromWatcher( - { - type: "file.watcher.updated", - properties: { - file: ".git/index.lock", - event: "change", - }, - }, - { - normalize: (input) => input, - hasFile: () => true, - loadFile: () => { - throw new Error("should not load") - }, - node: () => undefined, - isDirLoaded: () => true, - refreshDir: (path) => refresh.push(path), - }, - ) - - invalidateFromWatcher( - { - type: "project.updated", - properties: {}, - }, - { - normalize: (input) => input, - hasFile: () => false, - loadFile: () => {}, - node: () => undefined, - isDirLoaded: () => true, - refreshDir: (path) => refresh.push(path), - }, - ) - - expect(refresh).toEqual([]) - }) -}) diff --git a/packages/app/src/context/file/watcher.ts b/packages/app/src/context/file/watcher.ts deleted file mode 100644 index 57bc68edc9b..00000000000 --- a/packages/app/src/context/file/watcher.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { FileNode } from "@kilocode/sdk/v2" - -type WatcherEvent = { - type: string - properties: unknown -} - -type WatcherOps = { - normalize: (input: string) => string - hasFile: (path: string) => boolean - isOpen?: (path: string) => boolean - loadFile: (path: string) => void - node: (path: string) => FileNode | undefined - isDirLoaded: (path: string) => boolean - refreshDir: (path: string) => void -} - -export function invalidateFromWatcher(event: WatcherEvent, ops: WatcherOps) { - if (event.type !== "file.watcher.updated") return - const props = - typeof event.properties === "object" && event.properties ? (event.properties as Record) : undefined - const rawPath = typeof props?.file === "string" ? props.file : undefined - const kind = typeof props?.event === "string" ? props.event : undefined - if (!rawPath) return - if (!kind) return - - const path = ops.normalize(rawPath) - if (!path) return - if (path.startsWith(".git/")) return - - if (ops.hasFile(path) || ops.isOpen?.(path)) { - ops.loadFile(path) - } - - if (kind === "change") { - const dir = (() => { - if (path === "") return "" - const node = ops.node(path) - if (node?.type !== "directory") return - return path - })() - if (dir === undefined) return - if (!ops.isDirLoaded(dir)) return - ops.refreshDir(dir) - return - } - if (kind !== "add" && kind !== "unlink") return - - const parent = path.split("/").slice(0, -1).join("/") - if (!ops.isDirLoaded(parent)) return - - ops.refreshDir(parent) -} diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx deleted file mode 100644 index 5c81174426f..00000000000 --- a/packages/app/src/context/global-sdk.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import type { Event } from "@kilocode/sdk/v2/client" -import { createSimpleContext } from "@opencode-ai/ui/context" -import { createGlobalEmitter } from "@solid-primitives/event-bus" -import { makeEventListener } from "@solid-primitives/event-listener" -import { batch, onCleanup, onMount } from "solid-js" -import z from "zod" -import { createSdkForServer } from "@/utils/server" -import { useLanguage } from "./language" -import { usePlatform } from "./platform" -import { useServer } from "./server" - -const abortError = z.object({ - name: z.literal("AbortError"), -}) - -export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({ - name: "GlobalSDK", - init: () => { - const language = useLanguage() - const server = useServer() - const platform = usePlatform() - const abort = new AbortController() - - const eventFetch = (() => { - if (!platform.fetch || !server.current) return - try { - const url = new URL(server.current.http.url) - const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" - if (url.protocol === "http:" && !loopback) return platform.fetch - } catch { - return - } - })() - - const currentServer = server.current - if (!currentServer) throw new Error(language.t("error.globalSDK.noServerAvailable")) - - const eventSdk = createSdkForServer({ - signal: abort.signal, - fetch: eventFetch, - server: currentServer.http, - }) - const emitter = createGlobalEmitter<{ - [key: string]: Event - }>() - - type Queued = { directory: string; payload: Event } - const FLUSH_FRAME_MS = 16 - const STREAM_YIELD_MS = 8 - const RECONNECT_DELAY_MS = 250 - - let queue: Queued[] = [] - let buffer: Queued[] = [] - const coalesced = new Map() - const staleDeltas = new Set() - let timer: ReturnType | undefined - let last = 0 - - const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}` - - const key = (directory: string, payload: Event) => { - if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}` - if (payload.type === "lsp.updated") return `lsp.updated:${directory}` - if (payload.type === "message.part.updated") { - const part = payload.properties.part - return `message.part.updated:${directory}:${part.messageID}:${part.id}` - } - } - - const flush = () => { - if (timer) clearTimeout(timer) - timer = undefined - - if (queue.length === 0) return - - const events = queue - const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined - queue = buffer - buffer = events - queue.length = 0 - coalesced.clear() - staleDeltas.clear() - - last = Date.now() - batch(() => { - for (const event of events) { - if (skip && event.payload.type === "message.part.delta") { - const props = event.payload.properties - if (skip.has(deltaKey(event.directory, props.messageID, props.partID))) continue - } - emitter.emit(event.directory, event.payload) - } - }) - - buffer.length = 0 - } - - const schedule = () => { - if (timer) return - const elapsed = Date.now() - last - timer = setTimeout(flush, Math.max(0, FLUSH_FRAME_MS - elapsed)) - } - - let streamErrorLogged = false - const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) - const aborted = (error: unknown) => abortError.safeParse(error).success - - let attempt: AbortController | undefined - let run: Promise | undefined - let started = false - const HEARTBEAT_TIMEOUT_MS = 15_000 - let lastEventAt = Date.now() - let heartbeat: ReturnType | undefined - const resetHeartbeat = () => { - lastEventAt = Date.now() - if (heartbeat) clearTimeout(heartbeat) - heartbeat = setTimeout(() => { - attempt?.abort() - }, HEARTBEAT_TIMEOUT_MS) - } - const clearHeartbeat = () => { - if (!heartbeat) return - clearTimeout(heartbeat) - heartbeat = undefined - } - - const start = () => { - if (started) return run - started = true - run = (async () => { - // oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit - while (!abort.signal.aborted && started) { - attempt = new AbortController() - lastEventAt = Date.now() - const onAbort = () => { - attempt?.abort() - } - abort.signal.addEventListener("abort", onAbort) - try { - const events = await eventSdk.global.event({ - signal: attempt.signal, - onSseError: (error) => { - if (aborted(error)) return - if (streamErrorLogged) return - streamErrorLogged = true - console.error("[global-sdk] event stream error", { - url: currentServer.http.url, - fetch: eventFetch ? "platform" : "webview", - error, - }) - }, - }) - let yielded = Date.now() - resetHeartbeat() - for await (const event of events.stream) { - resetHeartbeat() - streamErrorLogged = false - const directory = event.directory ?? "global" - if (event.payload.type === "sync") { - continue - } - - const payload = event.payload as Event - - const k = key(directory, payload) - if (k) { - const i = coalesced.get(k) - if (i !== undefined) { - queue[i] = { directory, payload } - if (payload.type === "message.part.updated") { - const part = payload.properties.part - staleDeltas.add(deltaKey(directory, part.messageID, part.id)) - } - continue - } - coalesced.set(k, queue.length) - } - queue.push({ directory, payload }) - schedule() - - if (Date.now() - yielded < STREAM_YIELD_MS) continue - yielded = Date.now() - await wait(0) - } - } catch (error) { - if (!aborted(error) && !streamErrorLogged) { - streamErrorLogged = true - console.error("[global-sdk] event stream failed", { - url: currentServer.http.url, - fetch: eventFetch ? "platform" : "webview", - error, - }) - } - } finally { - abort.signal.removeEventListener("abort", onAbort) - attempt = undefined - clearHeartbeat() - } - - if (abort.signal.aborted || !started) return - await wait(RECONNECT_DELAY_MS) - } - })().finally(() => { - run = undefined - flush() - }) - return run - } - - const stop = () => { - started = false - attempt?.abort() - clearHeartbeat() - } - - onMount(() => { - makeEventListener(document, "visibilitychange", () => { - if (document.visibilityState !== "visible") return - if (!started) return - if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return - attempt?.abort() - }) - }) - - onCleanup(() => { - stop() - abort.abort() - flush() - }) - - const sdk = createSdkForServer({ - server: server.current.http, - fetch: platform.fetch, - throwOnError: true, - }) - - return { - url: currentServer.http.url, - client: sdk, - event: { - on: emitter.on.bind(emitter), - listen: emitter.listen.bind(emitter), - start, - }, - createClient(opts: Omit[0], "server" | "fetch">) { - const s = server.current - if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable")) - return createSdkForServer({ - server: s.http, - fetch: platform.fetch, - ...opts, - }) - }, - } - }, -}) diff --git a/packages/app/src/context/global-sync.test.ts b/packages/app/src/context/global-sync.test.ts deleted file mode 100644 index 93e9c417555..00000000000 --- a/packages/app/src/context/global-sync.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { canDisposeDirectory, pickDirectoriesToEvict } from "./global-sync/eviction" -import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load" - -describe("pickDirectoriesToEvict", () => { - test("keeps pinned stores and evicts idle stores", () => { - const now = 5_000 - const picks = pickDirectoriesToEvict({ - stores: ["a", "b", "c", "d"], - state: new Map([ - ["a", { lastAccessAt: 1_000 }], - ["b", { lastAccessAt: 4_900 }], - ["c", { lastAccessAt: 4_800 }], - ["d", { lastAccessAt: 3_000 }], - ]), - pins: new Set(["a"]), - max: 2, - ttl: 1_500, - now, - }) - - expect(picks).toEqual(["d", "c"]) - }) -}) - -describe("loadRootSessionsWithFallback", () => { - test("uses limited roots query when supported", async () => { - const calls: Array<{ directory: string; roots: true; limit?: number }> = [] - - const result = await loadRootSessionsWithFallback({ - directory: "dir", - limit: 10, - list: async (query) => { - calls.push(query) - return { data: [] } - }, - }) - - expect(result.data).toEqual([]) - expect(result.limited).toBe(true) - expect(calls).toEqual([{ directory: "dir", roots: true, limit: 10 }]) - }) - - test("falls back to full roots query on limited-query failure", async () => { - const calls: Array<{ directory: string; roots: true; limit?: number }> = [] - - const result = await loadRootSessionsWithFallback({ - directory: "dir", - limit: 25, - list: async (query) => { - calls.push(query) - if (query.limit) throw new Error("unsupported") - return { data: [] } - }, - }) - - expect(result.data).toEqual([]) - expect(result.limited).toBe(false) - expect(calls).toEqual([ - { directory: "dir", roots: true, limit: 25 }, - { directory: "dir", roots: true }, - ]) - }) -}) - -describe("estimateRootSessionTotal", () => { - test("keeps exact total for full fetches", () => { - expect(estimateRootSessionTotal({ count: 42, limit: 10, limited: false })).toBe(42) - }) - - test("marks has-more for full-limit limited fetches", () => { - expect(estimateRootSessionTotal({ count: 10, limit: 10, limited: true })).toBe(11) - }) - - test("keeps exact total when limited fetch is under limit", () => { - expect(estimateRootSessionTotal({ count: 9, limit: 10, limited: true })).toBe(9) - }) -}) - -describe("canDisposeDirectory", () => { - test("rejects pinned or inflight directories", () => { - expect( - canDisposeDirectory({ - directory: "dir", - hasStore: true, - pinned: true, - booting: false, - loadingSessions: false, - }), - ).toBe(false) - expect( - canDisposeDirectory({ - directory: "dir", - hasStore: true, - pinned: false, - booting: true, - loadingSessions: false, - }), - ).toBe(false) - expect( - canDisposeDirectory({ - directory: "dir", - hasStore: true, - pinned: false, - booting: false, - loadingSessions: true, - }), - ).toBe(false) - }) - - test("accepts idle unpinned directory store", () => { - expect( - canDisposeDirectory({ - directory: "dir", - hasStore: true, - pinned: false, - booting: false, - loadingSessions: false, - }), - ).toBe(true) - }) -}) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx deleted file mode 100644 index edebef2b909..00000000000 --- a/packages/app/src/context/global-sync.tsx +++ /dev/null @@ -1,424 +0,0 @@ -import type { - Config, - KiloClient, - Path, - Project, - ProviderAuthResponse, - ProviderListResponse, - Todo, -} from "@kilocode/sdk/v2/client" -import { showToast } from "@opencode-ai/ui/toast" -import { getFilename } from "@opencode-ai/shared/util/path" -import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js" -import { createStore, produce, reconcile } from "solid-js/store" -import { useLanguage } from "@/context/language" -import type { InitError } from "../pages/error" -import { useGlobalSDK } from "./global-sdk" -import { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap" -import { createChildStoreManager } from "./global-sync/child-store" -import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer" -import { createRefreshQueue } from "./global-sync/queue" -import { clearSessionPrefetchDirectory } from "./global-sync/session-prefetch" -import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load" -import { trimSessions } from "./global-sync/session-trim" -import type { ProjectMeta } from "./global-sync/types" -import { SESSION_RECENT_LIMIT } from "./global-sync/types" -import { formatServerError } from "@/utils/server-errors" -import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query" - -type GlobalStore = { - ready: boolean - error?: InitError - path: Path - project: Project[] - session_todo: { - [sessionID: string]: Todo[] - } - provider: ProviderListResponse - provider_auth: ProviderAuthResponse - config: Config - reload: undefined | "pending" | "complete" -} - -export const loadSessionsQuery = (directory: string) => - queryOptions({ queryKey: [directory, "loadSessions"], queryFn: skipToken }) - -function createGlobalSync() { - const globalSDK = useGlobalSDK() - const language = useLanguage() - const owner = getOwner() - if (!owner) throw new Error("GlobalSync must be created within owner") - - const sdkCache = new Map() - const booting = new Map>() - const sessionLoads = new Map>() - const sessionMeta = new Map() - - const [globalStore, setGlobalStore] = createStore({ - ready: false, - path: { state: "", config: "", worktree: "", directory: "", home: "" }, - project: [], - session_todo: {}, - provider: { all: [], connected: [], default: {} }, - provider_auth: {}, - config: {}, - reload: undefined, - }) - const queryClient = useQueryClient() - - let bootedAt = 0 - let bootingRoot = false - let eventFrame: number | undefined - let eventTimer: ReturnType | undefined - - onCleanup(() => { - if (eventFrame !== undefined) cancelAnimationFrame(eventFrame) - if (eventTimer !== undefined) clearTimeout(eventTimer) - }) - - const setProjects = (next: Project[] | ((draft: Project[]) => Project[])) => { - setGlobalStore("project", next) - } - - const setBootStore = ((...input: unknown[]) => { - if (input[0] === "project" && Array.isArray(input[1])) { - setProjects(input[1] as Project[]) - return input[1] - } - return (setGlobalStore as (...args: unknown[]) => unknown)(...input) - }) as typeof setGlobalStore - - const set = ((...input: unknown[]) => { - if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) { - setProjects(input[1] as Project[] | ((draft: Project[]) => Project[])) - return input[1] - } - return (setGlobalStore as (...args: unknown[]) => unknown)(...input) - }) as typeof setGlobalStore - - const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => { - if (!sessionID) return - if (!todos) { - setGlobalStore( - "session_todo", - produce((draft) => { - delete draft[sessionID] - }), - ) - return - } - setGlobalStore("session_todo", sessionID, reconcile(todos, { key: "id" })) - } - - const paused = () => untrack(() => globalStore.reload) !== undefined - - const queue = createRefreshQueue({ - paused, - bootstrap, - bootstrapInstance, - }) - - const children = createChildStoreManager({ - owner, - isBooting: (directory) => booting.has(directory), - isLoadingSessions: (directory) => sessionLoads.has(directory), - onBootstrap: (directory) => { - void bootstrapInstance(directory) - }, - onDispose: (directory) => { - queue.clear(directory) - sessionMeta.delete(directory) - sdkCache.delete(directory) - clearProviderRev(directory) - clearSessionPrefetchDirectory(directory) - }, - translate: language.t, - }) - - const sdkFor = (directory: string) => { - const cached = sdkCache.get(directory) - if (cached) return cached - const sdk = globalSDK.createClient({ - directory, - throwOnError: true, - }) - sdkCache.set(directory, sdk) - return sdk - } - - async function loadSessions(directory: string) { - const pending = sessionLoads.get(directory) - if (pending) return pending - - children.pin(directory) - const [store, setStore] = children.child(directory, { bootstrap: false }) - const meta = sessionMeta.get(directory) - if (meta && meta.limit >= store.limit) { - const next = trimSessions(store.session, { - limit: store.limit, - permission: store.permission, - }) - if (next.length !== store.session.length) { - setStore("session", reconcile(next, { key: "id" })) - cleanupDroppedSessionCaches(store, setStore, next, setSessionTodo) - } - children.unpin(directory) - return - } - - const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT) - const promise = queryClient - .fetchQuery({ - ...loadSessionsQuery(directory), - queryFn: () => - loadRootSessionsWithFallback({ - directory, - limit, - list: (query) => globalSDK.client.session.list(query), - }) - .then((x) => { - const nonArchived = (x.data ?? []) - .filter((s) => !!s?.id) - .filter((s) => !s.time?.archived) - .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) - const limit = store.limit - const childSessions = store.session.filter((s) => !!s.parentID) - const sessions = trimSessions([...nonArchived, ...childSessions], { - limit, - permission: store.permission, - }) - batch(() => { - setStore( - "sessionTotal", - estimateRootSessionTotal({ - count: nonArchived.length, - limit: x.limit, - limited: x.limited, - }), - ) - setStore("session", reconcile(sessions, { key: "id" })) - cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo) - }) - sessionMeta.set(directory, { limit }) - }) - .catch((err) => { - console.error("Failed to load sessions", err) - const project = getFilename(directory) - showToast({ - variant: "error", - title: language.t("toast.session.listFailed.title", { project }), - description: formatServerError(err, language.t), - }) - }) - .then(() => null), - }) - .then(() => {}) - - sessionLoads.set(directory, promise) - void promise.finally(() => { - sessionLoads.delete(directory) - children.unpin(directory) - }) - return promise - } - - async function bootstrapInstance(directory: string) { - if (!directory) return - const pending = booting.get(directory) - if (pending) return pending - - children.pin(directory) - const promise = Promise.resolve().then(async () => { - const child = children.ensureChild(directory) - const cache = children.vcsCache.get(directory) - if (!cache) return - const sdk = sdkFor(directory) - await bootstrapDirectory({ - directory, - global: { - config: globalStore.config, - path: globalStore.path, - project: globalStore.project, - provider: globalStore.provider, - }, - sdk, - store: child[0], - setStore: child[1], - vcsCache: cache, - loadSessions, - translate: language.t, - queryClient, - }) - }) - - booting.set(directory, promise) - void promise.finally(() => { - booting.delete(directory) - children.unpin(directory) - }) - return promise - } - - const unsub = globalSDK.event.listen((e) => { - const directory = e.name - const event = e.details - const recent = bootingRoot || Date.now() - bootedAt < 1500 - - if (event.type === "session.error") { - const error = event.properties.error - if (error?.name !== "MessageAbortedError") { - console.error("[global-sync] session error", { - scope: directory === "global" ? "global" : "workspace", - directory: directory === "global" ? undefined : directory, - project: directory === "global" ? undefined : getFilename(directory), - sessionID: event.properties.sessionID, - error, - }) - } - } - - if (directory === "global") { - applyGlobalEvent({ - event, - project: globalStore.project, - refresh: () => { - if (recent) return - queue.refresh() - }, - setGlobalProject: setProjects, - }) - if (event.type === "server.connected" || event.type === "global.disposed") { - if (recent) return - for (const directory of Object.keys(children.children)) { - queue.push(directory) - } - } - return - } - - const existing = children.children[directory] - if (!existing) return - children.mark(directory) - const [store, setStore] = existing - applyDirectoryEvent({ - event, - directory, - store, - setStore, - push: queue.push, - setSessionTodo, - vcsCache: children.vcsCache.get(directory), - loadLsp: () => { - void sdkFor(directory) - .lsp.status() - .then((x) => { - setStore("lsp", x.data ?? []) - setStore("lsp_ready", true) - }) - }, - }) - }) - - onCleanup(unsub) - onCleanup(() => { - queue.dispose() - }) - onCleanup(() => { - for (const directory of Object.keys(children.children)) { - children.disposeDirectory(directory) - } - }) - - async function bootstrap() { - bootingRoot = true - try { - await bootstrapGlobal({ - globalSDK: globalSDK.client, - requestFailedTitle: language.t("common.requestFailed"), - translate: language.t, - formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }), - setGlobalStore: setBootStore, - queryClient, - }) - bootedAt = Date.now() - } finally { - bootingRoot = false - } - } - - onMount(() => { - if (typeof requestAnimationFrame === "function") { - eventFrame = requestAnimationFrame(() => { - eventFrame = undefined - eventTimer = setTimeout(() => { - eventTimer = undefined - void globalSDK.event.start() - }, 0) - }) - } else { - eventTimer = setTimeout(() => { - eventTimer = undefined - void globalSDK.event.start() - }, 0) - } - void bootstrap() - }) - - const projectApi = { - loadSessions, - meta(directory: string, patch: ProjectMeta) { - children.projectMeta(directory, patch) - }, - icon(directory: string, value: string | undefined) { - children.projectIcon(directory, value) - }, - } - - const updateConfig = async (config: Config) => { - setGlobalStore("reload", "pending") - return globalSDK.client.global.config - .update({ config }) - .then(bootstrap) - .then(() => { - queue.refresh() - setGlobalStore("reload", undefined) - queue.refresh() - }) - .catch((error) => { - setGlobalStore("reload", undefined) - throw error - }) - } - - return { - data: globalStore, - set, - get ready() { - return globalStore.ready - }, - get error() { - return globalStore.error - }, - child: children.child, - peek: children.peek, - bootstrap, - updateConfig, - project: projectApi, - todo: { - set: setSessionTodo, - }, - } -} - -const GlobalSyncContext = createContext>() - -export function GlobalSyncProvider(props: ParentProps) { - const value = createGlobalSync() - return {props.children} -} - -export function useGlobalSync() { - const context = useContext(GlobalSyncContext) - if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider") - return context -} diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts deleted file mode 100644 index 0b4d843026a..00000000000 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ /dev/null @@ -1,386 +0,0 @@ -import type { - Config, - KiloClient, - Path, - PermissionRequest, - Project, - ProviderAuthResponse, - ProviderListResponse, - QuestionRequest, - Session, - Todo, -} from "@kilocode/sdk/v2/client" -import { showToast } from "@opencode-ai/ui/toast" -import { getFilename } from "@opencode-ai/shared/util/path" -import { retry } from "@opencode-ai/shared/util/retry" -import { batch } from "solid-js" -import { reconcile, type SetStoreFunction, type Store } from "solid-js/store" -import type { State, VcsCache } from "./types" -import { cmp, normalizeAgentList, normalizeProviderList } from "./utils" -import { formatServerError } from "@/utils/server-errors" -import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query" - -type GlobalStore = { - ready: boolean - path: Path - project: Project[] - session_todo: { - [sessionID: string]: Todo[] - } - provider: ProviderListResponse - provider_auth: ProviderAuthResponse - config: Config - reload: undefined | "pending" | "complete" -} - -function waitForPaint() { - return new Promise((resolve) => { - let done = false - const finish = () => { - if (done) return - done = true - resolve() - } - const timer = setTimeout(finish, 50) - if (typeof requestAnimationFrame !== "function") return - requestAnimationFrame(() => { - setTimeout(() => { - clearTimeout(timer) - finish() - }, 0) - }) - }) -} - -function errors(list: PromiseSettledResult[]) { - return list.filter((item): item is PromiseRejectedResult => item.status === "rejected").map((item) => item.reason) -} - -const providerRev = new Map() - -export function clearProviderRev(directory: string) { - providerRev.delete(directory) -} - -function runAll(list: Array<() => Promise>) { - return Promise.allSettled(list.map((item) => item())) -} - -export async function bootstrapGlobal(input: { - globalSDK: KiloClient - requestFailedTitle: string - translate: (key: string, vars?: Record) => string - formatMoreCount: (count: number) => string - setGlobalStore: SetStoreFunction - queryClient: QueryClient -}) { - const fast = [ - () => - retry(() => - input.globalSDK.global.config.get().then((x) => { - input.setGlobalStore("config", x.data!) - }), - ), - ] - - const slow = [ - () => - input.queryClient.fetchQuery({ - ...loadProvidersQuery(null), - queryFn: () => - retry(() => - input.globalSDK.provider.list().then((x) => { - input.setGlobalStore("provider", normalizeProviderList(x.data!)) - return null - }), - ), - }), - () => - retry(() => - input.globalSDK.path.get().then((x) => { - input.setGlobalStore("path", x.data!) - }), - ), - () => - retry(() => - input.globalSDK.project.list().then((x) => { - const projects = (x.data ?? []) - .filter((p) => !!p?.id) - .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) - .slice() - .sort((a, b) => cmp(a.id, b.id)) - input.setGlobalStore("project", projects) - }), - ), - ] - await runAll(fast) - // showErrors({ - // errors: errors(await runAll(fast)), - // title: input.requestFailedTitle, - // translate: input.translate, - // formatMoreCount: input.formatMoreCount, - // }) - await waitForPaint() - await runAll(slow) - // showErrors({ - // errors: errors(), - // title: input.requestFailedTitle, - // translate: input.translate, - // formatMoreCount: input.formatMoreCount, - // }) - input.setGlobalStore("ready", true) -} - -function groupBySession(input: T[]) { - return input.reduce>((acc, item) => { - if (!item?.id || !item.sessionID) return acc - const list = acc[item.sessionID] - if (list) list.push(item) - if (!list) acc[item.sessionID] = [item] - return acc - }, {}) -} - -function projectID(directory: string, projects: Project[]) { - return projects.find((project) => project.worktree === directory || project.sandboxes?.includes(directory))?.id -} - -function mergeSession(setStore: SetStoreFunction, session: Session) { - setStore("session", (list) => { - const next = list.slice() - const idx = next.findIndex((item) => item.id >= session.id) - if (idx === -1) return [...next, session] - if (next[idx]?.id === session.id) { - next[idx] = session - return next - } - next.splice(idx, 0, session) - return next - }) -} - -function warmSessions(input: { - ids: string[] - store: Store - setStore: SetStoreFunction - sdk: KiloClient -}) { - const known = new Set(input.store.session.map((item) => item.id)) - const ids = [...new Set(input.ids)].filter((id) => !!id && !known.has(id)) - if (ids.length === 0) return Promise.resolve() - return Promise.all( - ids.map((sessionID) => - retry(() => input.sdk.session.get({ sessionID })).then((x) => { - const session = x.data - if (!session?.id) return - mergeSession(input.setStore, session) - }), - ), - ).then(() => undefined) -} - -export const loadProvidersQuery = (directory: string | null) => - queryOptions({ queryKey: [directory, "providers"], queryFn: skipToken }) - -export const loadAgentsQuery = ( - directory: string | null, - sdk?: KiloClient, - transform?: (x: Awaited>) => void, -) => - queryOptions({ - queryKey: [directory, "agents"], - queryFn: - sdk && transform - ? () => - retry(() => - sdk.app - .agents() - .then(transform) - .then(() => null), - ) - : skipToken, - }) - -export const loadPathQuery = ( - directory: string | null, - sdk?: KiloClient, - transform?: (x: Awaited>) => void, -) => - queryOptions({ - queryKey: [directory, "path"], - queryFn: - sdk && transform - ? () => - retry(() => - sdk.path.get().then(async (x) => { - transform(x) - return x.data! - }), - ) - : skipToken, - }) - -export async function bootstrapDirectory(input: { - directory: string - sdk: KiloClient - store: Store - setStore: SetStoreFunction - vcsCache: VcsCache - loadSessions: (directory: string) => Promise | void - translate: (key: string, vars?: Record) => string - global: { - config: Config - path: Path - project: Project[] - provider: ProviderListResponse - } - queryClient: QueryClient -}) { - const loading = input.store.status !== "complete" - const seededProject = projectID(input.directory, input.global.project) - const seededPath = input.global.path.directory === input.directory ? input.global.path : undefined - if (seededProject) input.setStore("project", seededProject) - if (seededPath) input.setStore("path", seededPath) - if (input.store.provider.all.length === 0 && input.global.provider.all.length > 0) { - input.setStore("provider", input.global.provider) - } - if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) { - input.setStore("config", input.global.config) - } - if (loading || input.store.provider.all.length === 0) { - input.setStore("provider_ready", false) - } - input.setStore("mcp_ready", false) - input.setStore("mcp", {}) - input.setStore("lsp_ready", false) - input.setStore("lsp", []) - if (loading) input.setStore("status", "partial") - - const rev = (providerRev.get(input.directory) ?? 0) + 1 - providerRev.set(input.directory, rev) - ;(async () => { - const slow = [ - () => Promise.resolve(input.loadSessions(input.directory)), - () => - input.queryClient.ensureQueryData( - loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))), - ), - () => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))), - () => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))), - !seededProject && - (() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))), - !seededPath && - (() => - input.queryClient.ensureQueryData( - loadPathQuery(input.directory, input.sdk, (x) => { - const next = projectID(x.data?.directory ?? input.directory, input.global.project) - if (next) input.setStore("project", next) - }), - )), - () => - retry(() => - input.sdk.vcs.get().then((x) => { - const next = x.data ?? input.store.vcs - input.setStore("vcs", next) - if (next) input.vcsCache.setStore("value", next) - }), - ), - () => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))), - () => - retry(() => - input.sdk.permission.list().then((x) => { - const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id) - const grouped = groupBySession( - (x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID), - ) - return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() => - batch(() => { - for (const sessionID of Object.keys(input.store.permission)) { - if (grouped[sessionID]) continue - input.setStore("permission", sessionID, []) - } - for (const [sessionID, permissions] of Object.entries(grouped)) { - input.setStore( - "permission", - sessionID, - reconcile( - permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)), - { key: "id" }, - ), - ) - } - }), - ) - }), - ), - () => - retry(() => - input.sdk.question.list().then((x) => { - const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id) - const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID)) - return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() => - batch(() => { - for (const sessionID of Object.keys(input.store.question)) { - if (grouped[sessionID]) continue - input.setStore("question", sessionID, []) - } - for (const [sessionID, questions] of Object.entries(grouped)) { - input.setStore( - "question", - sessionID, - reconcile( - questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)), - { key: "id" }, - ), - ) - } - }), - ) - }), - ), - () => Promise.resolve(input.loadSessions(input.directory)), - () => - retry(() => - input.sdk.mcp.status().then((x) => { - input.setStore("mcp", x.data!) - input.setStore("mcp_ready", true) - }), - ), - () => - input.queryClient.ensureQueryData({ - ...loadProvidersQuery(input.directory), - queryFn: () => - retry(() => input.sdk.provider.list()) - .then((x) => { - if (providerRev.get(input.directory) !== rev) return - input.setStore("provider", normalizeProviderList(x.data!)) - input.setStore("provider_ready", true) - }) - .catch((err) => { - if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err) - const project = getFilename(input.directory) - showToast({ - variant: "error", - title: input.translate("toast.project.reloadFailed.title", { project }), - description: formatServerError(err, input.translate), - }) - }) - .then(() => null), - }), - ].filter(Boolean) as (() => Promise)[] - - await waitForPaint() - const slowErrs = errors(await runAll(slow)) - if (slowErrs.length > 0) { - console.error("Failed to finish bootstrap instance", slowErrs[0]) - const project = getFilename(input.directory) - showToast({ - variant: "error", - title: input.translate("toast.project.reloadFailed.title", { project }), - description: formatServerError(slowErrs[0], input.translate), - }) - } - - if (loading && slowErrs.length === 0) input.setStore("status", "complete") - })() -} diff --git a/packages/app/src/context/global-sync/child-store.test.ts b/packages/app/src/context/global-sync/child-store.test.ts deleted file mode 100644 index eee763f16de..00000000000 --- a/packages/app/src/context/global-sync/child-store.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { createRoot, getOwner } from "solid-js" -import { createStore } from "solid-js/store" -import type { State } from "./types" -import { createChildStoreManager } from "./child-store" - -const child = () => createStore({} as State) - -describe("createChildStoreManager", () => { - test("does not evict the active directory during mark", () => { - const owner = createRoot((dispose) => { - const current = getOwner() - dispose() - return current - }) - if (!owner) throw new Error("owner required") - - const manager = createChildStoreManager({ - owner, - isBooting: () => false, - isLoadingSessions: () => false, - onBootstrap() {}, - onDispose() {}, - translate: (key) => key, - }) - - Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => { - manager.children[directory] = child() - manager.pin(directory) - }) - - const directory = "/active" - manager.children[directory] = child() - manager.mark(directory) - - expect(manager.children[directory]).toBeDefined() - }) -}) diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts deleted file mode 100644 index 1545fb42089..00000000000 --- a/packages/app/src/context/global-sync/child-store.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js" -import { createStore, type SetStoreFunction, type Store } from "solid-js/store" -import { Persist, persisted } from "@/utils/persist" -import type { VcsInfo } from "@kilocode/sdk/v2/client" -import { - DIR_IDLE_TTL_MS, - MAX_DIR_STORES, - type ChildOptions, - type DirState, - type IconCache, - type MetaCache, - type ProjectMeta, - type State, - type VcsCache, -} from "./types" -import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction" -import { useQuery } from "@tanstack/solid-query" -import { loadPathQuery } from "./bootstrap" - -export function createChildStoreManager(input: { - owner: Owner - isBooting: (directory: string) => boolean - isLoadingSessions: (directory: string) => boolean - onBootstrap: (directory: string) => void - onDispose: (directory: string) => void - translate: (key: string, vars?: Record) => string -}) { - const children: Record, SetStoreFunction]> = {} - const vcsCache = new Map() - const metaCache = new Map() - const iconCache = new Map() - const lifecycle = new Map() - const pins = new Map() - const ownerPins = new WeakMap>() - const disposers = new Map void>() - - const mark = (directory: string) => { - if (!directory) return - lifecycle.set(directory, { lastAccessAt: Date.now() }) - runEviction(directory) - } - - const pin = (directory: string) => { - if (!directory) return - pins.set(directory, (pins.get(directory) ?? 0) + 1) - mark(directory) - } - - const unpin = (directory: string) => { - if (!directory) return - const next = (pins.get(directory) ?? 0) - 1 - if (next > 0) { - pins.set(directory, next) - return - } - pins.delete(directory) - runEviction() - } - - const pinned = (directory: string) => (pins.get(directory) ?? 0) > 0 - - const pinForOwner = (directory: string) => { - const current = getOwner() - if (!current) return - if (current === input.owner) return - const key = current as object - const set = ownerPins.get(key) - if (set?.has(directory)) return - if (set) set.add(directory) - if (!set) ownerPins.set(key, new Set([directory])) - pin(directory) - onCleanup(() => { - const set = ownerPins.get(key) - if (set) { - set.delete(directory) - if (set.size === 0) ownerPins.delete(key) - } - unpin(directory) - }) - } - - function disposeDirectory(directory: string) { - if ( - !canDisposeDirectory({ - directory, - hasStore: !!children[directory], - pinned: pinned(directory), - booting: input.isBooting(directory), - loadingSessions: input.isLoadingSessions(directory), - }) - ) { - return false - } - - vcsCache.delete(directory) - metaCache.delete(directory) - iconCache.delete(directory) - lifecycle.delete(directory) - const dispose = disposers.get(directory) - if (dispose) { - dispose() - disposers.delete(directory) - } - delete children[directory] - input.onDispose(directory) - return true - } - - function runEviction(skip?: string) { - const stores = Object.keys(children) - if (stores.length === 0) return - const list = pickDirectoriesToEvict({ - stores, - state: lifecycle, - pins: new Set(stores.filter(pinned)), - max: MAX_DIR_STORES, - ttl: DIR_IDLE_TTL_MS, - now: Date.now(), - }).filter((directory) => directory !== skip) - if (list.length === 0) return - for (const directory of list) { - if (!disposeDirectory(directory)) continue - } - } - - function ensureChild(directory: string) { - if (!directory) console.error("No directory provided") - if (!children[directory]) { - const vcs = runWithOwner(input.owner, () => - persisted( - Persist.workspace(directory, "vcs", ["vcs.v1"]), - createStore({ value: undefined as VcsInfo | undefined }), - ), - ) - if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed")) - const vcsStore = vcs[0] - vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcs[3] }) - - const meta = runWithOwner(input.owner, () => - persisted( - Persist.workspace(directory, "project", ["project.v1"]), - createStore({ value: undefined as ProjectMeta | undefined }), - ), - ) - if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed")) - metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] }) - - const icon = runWithOwner(input.owner, () => - persisted( - Persist.workspace(directory, "icon", ["icon.v1"]), - createStore({ value: undefined as string | undefined }), - ), - ) - if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed")) - iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] }) - - const init = () => - createRoot((dispose) => { - const initialIcon = icon[0].value - - const pathQuery = useQuery(() => loadPathQuery(directory)) - const child = createStore({ - project: "", - projectMeta: undefined, - icon: initialIcon, - provider_ready: false, - provider: { all: [], connected: [], default: {} }, - config: {}, - get path() { - if (pathQuery.isLoading || !pathQuery.data) - return { state: "", config: "", worktree: "", directory: "", home: "" } - return pathQuery.data - }, - status: "loading" as const, - agent: [], - command: [], - session: [], - sessionTotal: 0, - session_status: {}, - session_diff: {}, - todo: {}, - permission: {}, - question: {}, - mcp_ready: false, - mcp: {}, - lsp_ready: false, - lsp: [], - vcs: vcsStore.value, - limit: 5, - message: {}, - part: {}, - }) - children[directory] = child - disposers.set(directory, dispose) - - const onPersistedInit = (init: Promise | string | null, run: () => void) => { - if (!(init instanceof Promise)) return - void init.then(() => { - if (children[directory] !== child) return - run() - }) - } - - onPersistedInit(vcs[2], () => { - const cached = vcsStore.value - if (!cached?.branch) return - child[1]("vcs", (value) => value ?? cached) - }) - - onPersistedInit(icon[2], () => { - if (child[0].icon !== initialIcon) return - child[1]("icon", icon[0].value) - }) - }) - - runWithOwner(input.owner, init) - } - mark(directory) - const childStore = children[directory] - if (!childStore) throw new Error(input.translate("error.childStore.storeCreateFailed")) - return childStore - } - - function child(directory: string, options: ChildOptions = {}) { - const childStore = ensureChild(directory) - pinForOwner(directory) - const shouldBootstrap = options.bootstrap ?? true - if (shouldBootstrap && childStore[0].status === "loading") { - input.onBootstrap(directory) - } - return childStore - } - - function peek(directory: string, options: ChildOptions = {}) { - const childStore = ensureChild(directory) - const shouldBootstrap = options.bootstrap ?? true - if (shouldBootstrap && childStore[0].status === "loading") { - input.onBootstrap(directory) - } - return childStore - } - - function projectMeta(directory: string, patch: ProjectMeta) { - const [store, setStore] = ensureChild(directory) - const cached = metaCache.get(directory) - if (!cached) return - const previous = store.projectMeta ?? {} - const icon = patch.icon ? { ...previous.icon, ...patch.icon } : previous.icon - const commands = patch.commands ? { ...previous.commands, ...patch.commands } : previous.commands - const next = { - ...previous, - ...patch, - icon, - commands, - } - cached.setStore("value", next) - setStore("projectMeta", next) - } - - function projectIcon(directory: string, value: string | undefined) { - const [store, setStore] = ensureChild(directory) - const cached = iconCache.get(directory) - if (!cached) return - if (store.icon === value) return - cached.setStore("value", value) - setStore("icon", value) - } - - return { - children, - ensureChild, - child, - peek, - projectMeta, - projectIcon, - mark, - pin, - unpin, - pinned, - disposeDirectory, - runEviction, - vcsCache, - metaCache, - iconCache, - } -} diff --git a/packages/app/src/context/global-sync/event-reducer.test.ts b/packages/app/src/context/global-sync/event-reducer.test.ts deleted file mode 100644 index f66e7ede312..00000000000 --- a/packages/app/src/context/global-sync/event-reducer.test.ts +++ /dev/null @@ -1,554 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { Message, Part, PermissionRequest, Project, QuestionRequest, Session } from "@kilocode/sdk/v2/client" -import { createStore } from "solid-js/store" -import type { State } from "./types" -import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./event-reducer" - -const rootSession = (input: { id: string; parentID?: string; archived?: number }) => - ({ - id: input.id, - parentID: input.parentID, - time: { - created: 1, - updated: 1, - archived: input.archived, - }, - }) as Session - -const userMessage = (id: string, sessionID: string) => - ({ - id, - sessionID, - role: "user", - time: { created: 1 }, - agent: "assistant", - model: { providerID: "openai", modelID: "gpt" }, - }) as Message - -const textPart = (id: string, sessionID: string, messageID: string) => - ({ - id, - sessionID, - messageID, - type: "text", - text: id, - }) as Part - -const permissionRequest = (id: string, sessionID: string, title = id) => - ({ - id, - sessionID, - permission: title, - patterns: ["*"], - metadata: {}, - always: [], - }) as PermissionRequest - -const questionRequest = (id: string, sessionID: string, title = id) => - ({ - id, - sessionID, - questions: [ - { - question: title, - header: title, - options: [{ label: title, description: title }], - }, - ], - }) as QuestionRequest - -const baseState = (input: Partial = {}) => - ({ - status: "complete", - agent: [], - command: [], - project: "", - projectMeta: undefined, - icon: undefined, - provider: {} as State["provider"], - config: {} as State["config"], - path: { directory: "/tmp" } as State["path"], - session: [], - sessionTotal: 0, - session_status: {}, - session_diff: {}, - todo: {}, - permission: {}, - question: {}, - mcp: {}, - lsp: [], - vcs: undefined, - limit: 10, - message: {}, - part: {}, - ...input, - }) as State - -describe("applyGlobalEvent", () => { - test("upserts project.updated in sorted position", () => { - const project = [{ id: "a" }, { id: "c" }] as Project[] - let refreshCount = 0 - applyGlobalEvent({ - event: { type: "project.updated", properties: { id: "b" } }, - project, - refresh: () => { - refreshCount += 1 - }, - setGlobalProject(next) { - if (typeof next === "function") next(project) - }, - }) - - expect(project.map((x) => x.id)).toEqual(["a", "b", "c"]) - expect(refreshCount).toBe(0) - }) - - test("handles global.disposed by triggering refresh", () => { - let refreshCount = 0 - applyGlobalEvent({ - event: { type: "global.disposed" }, - project: [], - refresh: () => { - refreshCount += 1 - }, - setGlobalProject() {}, - }) - - expect(refreshCount).toBe(1) - }) - - test("handles server.connected by triggering refresh", () => { - let refreshCount = 0 - applyGlobalEvent({ - event: { type: "server.connected" }, - project: [], - refresh: () => { - refreshCount += 1 - }, - setGlobalProject() {}, - }) - - expect(refreshCount).toBe(1) - }) -}) - -describe("applyDirectoryEvent", () => { - test("inserts root sessions in sorted order and updates sessionTotal", () => { - const [store, setStore] = createStore( - baseState({ - session: [rootSession({ id: "b" })], - sessionTotal: 1, - }), - ) - - applyDirectoryEvent({ - event: { type: "session.created", properties: { info: rootSession({ id: "a" }) } }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - - expect(store.session.map((x) => x.id)).toEqual(["a", "b"]) - expect(store.sessionTotal).toBe(2) - - applyDirectoryEvent({ - event: { type: "session.created", properties: { info: rootSession({ id: "c", parentID: "a" }) } }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - - expect(store.sessionTotal).toBe(2) - }) - - test("cleans session caches when archived", () => { - const message = userMessage("msg_1", "ses_1") - const [store, setStore] = createStore( - baseState({ - session: [rootSession({ id: "ses_1" }), rootSession({ id: "ses_2" })], - sessionTotal: 2, - message: { ses_1: [message] }, - part: { [message.id]: [textPart("prt_1", "ses_1", message.id)] }, - session_diff: { ses_1: [] }, - todo: { ses_1: [] }, - permission: { ses_1: [] }, - question: { ses_1: [] }, - session_status: { ses_1: { type: "busy" } }, - }), - ) - - applyDirectoryEvent({ - event: { type: "session.updated", properties: { info: rootSession({ id: "ses_1", archived: 10 }) } }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - - expect(store.session.map((x) => x.id)).toEqual(["ses_2"]) - expect(store.sessionTotal).toBe(1) - expect(store.message.ses_1).toBeUndefined() - expect(store.part[message.id]).toBeUndefined() - expect(store.session_diff.ses_1).toBeUndefined() - expect(store.todo.ses_1).toBeUndefined() - expect(store.permission.ses_1).toBeUndefined() - expect(store.question.ses_1).toBeUndefined() - expect(store.session_status.ses_1).toBeUndefined() - }) - - test("cleans session caches when deleted and decrements only root totals", () => { - const cases = [ - { info: rootSession({ id: "ses_1" }), expectedTotal: 1 }, - { info: rootSession({ id: "ses_2", parentID: "ses_1" }), expectedTotal: 2 }, - ] - - for (const item of cases) { - const message = userMessage("msg_1", item.info.id) - const [store, setStore] = createStore( - baseState({ - session: [ - rootSession({ id: "ses_1" }), - rootSession({ id: "ses_2", parentID: "ses_1" }), - rootSession({ id: "ses_3" }), - ], - sessionTotal: 2, - message: { [item.info.id]: [message] }, - part: { [message.id]: [textPart("prt_1", item.info.id, message.id)] }, - session_diff: { [item.info.id]: [] }, - todo: { [item.info.id]: [] }, - permission: { [item.info.id]: [] }, - question: { [item.info.id]: [] }, - session_status: { [item.info.id]: { type: "busy" } }, - }), - ) - - applyDirectoryEvent({ - event: { type: "session.deleted", properties: { info: item.info } }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - - expect(store.session.find((x) => x.id === item.info.id)).toBeUndefined() - expect(store.sessionTotal).toBe(item.expectedTotal) - expect(store.message[item.info.id]).toBeUndefined() - expect(store.part[message.id]).toBeUndefined() - expect(store.session_diff[item.info.id]).toBeUndefined() - expect(store.todo[item.info.id]).toBeUndefined() - expect(store.permission[item.info.id]).toBeUndefined() - expect(store.question[item.info.id]).toBeUndefined() - expect(store.session_status[item.info.id]).toBeUndefined() - } - }) - - test("cleans caches for trimmed sessions on session.created", () => { - const dropped = rootSession({ id: "ses_b" }) - const kept = rootSession({ id: "ses_a" }) - const message = userMessage("msg_1", dropped.id) - const todos: string[] = [] - const [store, setStore] = createStore( - baseState({ - limit: 1, - session: [dropped], - message: { [dropped.id]: [message] }, - part: { [message.id]: [textPart("prt_1", dropped.id, message.id)] }, - session_diff: { [dropped.id]: [] }, - todo: { [dropped.id]: [] }, - permission: { [dropped.id]: [] }, - question: { [dropped.id]: [] }, - session_status: { [dropped.id]: { type: "busy" } }, - }), - ) - - applyDirectoryEvent({ - event: { type: "session.created", properties: { info: kept } }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - setSessionTodo(sessionID, value) { - if (value !== undefined) return - todos.push(sessionID) - }, - }) - - expect(store.session.map((x) => x.id)).toEqual([kept.id]) - expect(store.message[dropped.id]).toBeUndefined() - expect(store.part[message.id]).toBeUndefined() - expect(store.session_diff[dropped.id]).toBeUndefined() - expect(store.todo[dropped.id]).toBeUndefined() - expect(store.permission[dropped.id]).toBeUndefined() - expect(store.question[dropped.id]).toBeUndefined() - expect(store.session_status[dropped.id]).toBeUndefined() - expect(todos).toEqual([dropped.id]) - }) - - test("cleanupDroppedSessionCaches clears part-only orphan state", () => { - const [store, setStore] = createStore( - baseState({ - session: [rootSession({ id: "ses_keep" })], - part: { msg_1: [textPart("prt_1", "ses_drop", "msg_1")] }, - }), - ) - - cleanupDroppedSessionCaches(store, setStore, store.session) - - expect(store.part.msg_1).toBeUndefined() - }) - - test("upserts and removes messages while clearing orphaned parts", () => { - const sessionID = "ses_1" - const [store, setStore] = createStore( - baseState({ - message: { [sessionID]: [userMessage("msg_1", sessionID), userMessage("msg_3", sessionID)] }, - part: { msg_2: [textPart("prt_1", sessionID, "msg_2")] }, - }), - ) - - applyDirectoryEvent({ - event: { type: "message.updated", properties: { info: userMessage("msg_2", sessionID) } }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - - expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2", "msg_3"]) - - applyDirectoryEvent({ - event: { - type: "message.updated", - properties: { - info: { - ...userMessage("msg_2", sessionID), - role: "assistant", - } as Message, - }, - }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - - expect(store.message[sessionID]?.find((x) => x.id === "msg_2")?.role).toBe("assistant") - - applyDirectoryEvent({ - event: { type: "message.removed", properties: { sessionID, messageID: "msg_2" } }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - - expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_3"]) - expect(store.part.msg_2).toBeUndefined() - }) - - test("upserts and prunes message parts", () => { - const sessionID = "ses_1" - const messageID = "msg_1" - const [store, setStore] = createStore( - baseState({ - part: { [messageID]: [textPart("prt_1", sessionID, messageID), textPart("prt_3", sessionID, messageID)] }, - }), - ) - - applyDirectoryEvent({ - event: { type: "message.part.updated", properties: { part: textPart("prt_2", sessionID, messageID) } }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - expect(store.part[messageID]?.map((x) => x.id)).toEqual(["prt_1", "prt_2", "prt_3"]) - - applyDirectoryEvent({ - event: { - type: "message.part.updated", - properties: { - part: { - ...textPart("prt_2", sessionID, messageID), - text: "changed", - } as Part, - }, - }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - const updated = store.part[messageID]?.find((x) => x.id === "prt_2") - expect(updated?.type).toBe("text") - if (updated?.type === "text") expect(updated.text).toBe("changed") - - applyDirectoryEvent({ - event: { type: "message.part.removed", properties: { messageID, partID: "prt_1" } }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - applyDirectoryEvent({ - event: { type: "message.part.removed", properties: { messageID, partID: "prt_2" } }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - applyDirectoryEvent({ - event: { type: "message.part.removed", properties: { messageID, partID: "prt_3" } }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - - expect(store.part[messageID]).toBeUndefined() - }) - - test("tracks permission and question request lifecycles", () => { - const sessionID = "ses_1" - const [store, setStore] = createStore( - baseState({ - permission: { [sessionID]: [permissionRequest("perm_1", sessionID), permissionRequest("perm_3", sessionID)] }, - question: { [sessionID]: [questionRequest("q_1", sessionID), questionRequest("q_3", sessionID)] }, - }), - ) - - applyDirectoryEvent({ - event: { type: "permission.asked", properties: permissionRequest("perm_2", sessionID) }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - expect(store.permission[sessionID]?.map((x) => x.id)).toEqual(["perm_1", "perm_2", "perm_3"]) - - applyDirectoryEvent({ - event: { type: "permission.asked", properties: permissionRequest("perm_2", sessionID, "updated") }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - expect(store.permission[sessionID]?.find((x) => x.id === "perm_2")?.permission).toBe("updated") - - applyDirectoryEvent({ - event: { type: "permission.replied", properties: { sessionID, requestID: "perm_2" } }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - expect(store.permission[sessionID]?.map((x) => x.id)).toEqual(["perm_1", "perm_3"]) - - applyDirectoryEvent({ - event: { type: "question.asked", properties: questionRequest("q_2", sessionID) }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - expect(store.question[sessionID]?.map((x) => x.id)).toEqual(["q_1", "q_2", "q_3"]) - - applyDirectoryEvent({ - event: { type: "question.asked", properties: questionRequest("q_2", sessionID, "updated") }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - expect(store.question[sessionID]?.find((x) => x.id === "q_2")?.questions[0]?.header).toBe("updated") - - applyDirectoryEvent({ - event: { type: "question.rejected", properties: { sessionID, requestID: "q_2" } }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - }) - expect(store.question[sessionID]?.map((x) => x.id)).toEqual(["q_1", "q_3"]) - }) - - test("updates vcs branch in store and cache", () => { - const [store, setStore] = createStore(baseState({ vcs: { branch: "main", default_branch: "main" } })) - const [cacheStore, setCacheStore] = createStore({ - value: { branch: "main", default_branch: "main" } as State["vcs"], - }) - - applyDirectoryEvent({ - event: { type: "vcs.branch.updated", properties: { branch: "feature/test" } }, - store, - setStore, - push() {}, - directory: "/tmp", - loadLsp() {}, - vcsCache: { - store: cacheStore, - setStore: setCacheStore, - ready: () => true, - }, - }) - - expect(store.vcs).toEqual({ branch: "feature/test", default_branch: "main" }) - expect(cacheStore.value).toEqual({ branch: "feature/test", default_branch: "main" }) - }) - - test("routes disposal and lsp events to side-effect handlers", () => { - const [store, setStore] = createStore(baseState()) - const pushes: string[] = [] - let lspLoads = 0 - - applyDirectoryEvent({ - event: { type: "server.instance.disposed" }, - store, - setStore, - push(directory) { - pushes.push(directory) - }, - directory: "/tmp", - loadLsp() { - lspLoads += 1 - }, - }) - - applyDirectoryEvent({ - event: { type: "lsp.updated" }, - store, - setStore, - push(directory) { - pushes.push(directory) - }, - directory: "/tmp", - loadLsp() { - lspLoads += 1 - }, - }) - - expect(pushes).toEqual(["/tmp"]) - expect(lspLoads).toBe(1) - }) -}) diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts deleted file mode 100644 index 9196d5b4f76..00000000000 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ /dev/null @@ -1,364 +0,0 @@ -import { Binary } from "@opencode-ai/shared/util/binary" -import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store" -import type { - Message, - Part, - PermissionRequest, - Project, - QuestionRequest, - Session, - SessionStatus, - SnapshotFileDiff, - Todo, -} from "@kilocode/sdk/v2/client" -import type { State, VcsCache } from "./types" -import { trimSessions } from "./session-trim" -import { dropSessionCaches } from "./session-cache" -import { diffs as list, message as clean } from "@/utils/diffs" - -const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) - -export function applyGlobalEvent(input: { - event: { type: string; properties?: unknown } - project: Project[] - setGlobalProject: (next: Project[] | ((draft: Project[]) => Project[])) => void - refresh: () => void -}) { - if (input.event.type === "global.disposed" || input.event.type === "server.connected") { - input.refresh() - return - } - - if (input.event.type !== "project.updated") return - const properties = input.event.properties as Project - const result = Binary.search(input.project, properties.id, (s) => s.id) - if (result.found) { - input.setGlobalProject( - produce((draft) => { - draft[result.index] = { ...draft[result.index], ...properties } - }), - ) - return - } - input.setGlobalProject( - produce((draft) => { - draft.splice(result.index, 0, properties) - }), - ) -} - -function cleanupSessionCaches( - setStore: SetStoreFunction, - sessionID: string, - setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void, -) { - if (!sessionID) return - setSessionTodo?.(sessionID, undefined) - setStore( - produce((draft) => { - dropSessionCaches(draft, [sessionID]) - }), - ) -} - -export function cleanupDroppedSessionCaches( - store: Store, - setStore: SetStoreFunction, - next: Session[], - setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void, -) { - const keep = new Set(next.map((item) => item.id)) - const stale = [ - ...Object.keys(store.message), - ...Object.keys(store.session_diff), - ...Object.keys(store.todo), - ...Object.keys(store.permission), - ...Object.keys(store.question), - ...Object.keys(store.session_status), - ...Object.values(store.part) - .map((parts) => parts?.find((part) => !!part?.sessionID)?.sessionID) - .filter((sessionID): sessionID is string => !!sessionID), - ].filter((sessionID, index, list) => !keep.has(sessionID) && list.indexOf(sessionID) === index) - if (stale.length === 0) return - for (const sessionID of stale) { - setSessionTodo?.(sessionID, undefined) - } - setStore( - produce((draft) => { - dropSessionCaches(draft, stale) - }), - ) -} - -export function applyDirectoryEvent(input: { - event: { type: string; properties?: unknown } - store: Store - setStore: SetStoreFunction - push: (directory: string) => void - directory: string - loadLsp: () => void - vcsCache?: VcsCache - setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void -}) { - const event = input.event - switch (event.type) { - case "server.instance.disposed": { - input.push(input.directory) - return - } - case "session.created": { - const info = (event.properties as { info: Session }).info - const result = Binary.search(input.store.session, info.id, (s) => s.id) - if (result.found) { - input.setStore("session", result.index, reconcile(info)) - break - } - const next = input.store.session.slice() - next.splice(result.index, 0, info) - const trimmed = trimSessions(next, { limit: input.store.limit, permission: input.store.permission }) - input.setStore("session", reconcile(trimmed, { key: "id" })) - cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo) - if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1) - break - } - case "session.updated": { - const info = (event.properties as { info: Session }).info - const result = Binary.search(input.store.session, info.id, (s) => s.id) - if (info.time.archived) { - if (result.found) { - input.setStore( - "session", - produce((draft) => { - draft.splice(result.index, 1) - }), - ) - } - cleanupSessionCaches(input.setStore, info.id, input.setSessionTodo) - if (info.parentID) break - input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) - break - } - if (result.found) { - input.setStore("session", result.index, reconcile(info)) - break - } - const next = input.store.session.slice() - next.splice(result.index, 0, info) - const trimmed = trimSessions(next, { limit: input.store.limit, permission: input.store.permission }) - input.setStore("session", reconcile(trimmed, { key: "id" })) - cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo) - break - } - case "session.deleted": { - const info = (event.properties as { info: Session }).info - const result = Binary.search(input.store.session, info.id, (s) => s.id) - if (result.found) { - input.setStore( - "session", - produce((draft) => { - draft.splice(result.index, 1) - }), - ) - } - cleanupSessionCaches(input.setStore, info.id, input.setSessionTodo) - if (info.parentID) break - input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) - break - } - case "session.diff": { - const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] } - input.setStore("session_diff", props.sessionID, reconcile(list(props.diff), { key: "file" })) - break - } - case "todo.updated": { - const props = event.properties as { sessionID: string; todos: Todo[] } - input.setStore("todo", props.sessionID, reconcile(props.todos, { key: "id" })) - input.setSessionTodo?.(props.sessionID, props.todos) - break - } - case "session.status": { - const props = event.properties as { sessionID: string; status: SessionStatus } - input.setStore("session_status", props.sessionID, reconcile(props.status)) - break - } - case "message.updated": { - const info = clean((event.properties as { info: Message }).info) - const messages = input.store.message[info.sessionID] - if (!messages) { - input.setStore("message", info.sessionID, [info]) - break - } - const result = Binary.search(messages, info.id, (m) => m.id) - if (result.found) { - input.setStore("message", info.sessionID, result.index, reconcile(info)) - break - } - input.setStore( - "message", - info.sessionID, - produce((draft) => { - draft.splice(result.index, 0, info) - }), - ) - break - } - case "message.removed": { - const props = event.properties as { sessionID: string; messageID: string } - input.setStore( - produce((draft) => { - const messages = draft.message[props.sessionID] - if (messages) { - const result = Binary.search(messages, props.messageID, (m) => m.id) - if (result.found) messages.splice(result.index, 1) - } - delete draft.part[props.messageID] - }), - ) - break - } - case "message.part.updated": { - const part = (event.properties as { part: Part }).part - if (SKIP_PARTS.has(part.type)) break - const parts = input.store.part[part.messageID] - if (!parts) { - input.setStore("part", part.messageID, [part]) - break - } - const result = Binary.search(parts, part.id, (p) => p.id) - if (result.found) { - input.setStore("part", part.messageID, result.index, reconcile(part)) - break - } - input.setStore( - "part", - part.messageID, - produce((draft) => { - draft.splice(result.index, 0, part) - }), - ) - break - } - case "message.part.removed": { - const props = event.properties as { messageID: string; partID: string } - const parts = input.store.part[props.messageID] - if (!parts) break - const result = Binary.search(parts, props.partID, (p) => p.id) - if (result.found) { - input.setStore( - produce((draft) => { - const list = draft.part[props.messageID] - if (!list) return - const next = Binary.search(list, props.partID, (p) => p.id) - if (!next.found) return - list.splice(next.index, 1) - if (list.length === 0) delete draft.part[props.messageID] - }), - ) - } - break - } - case "message.part.delta": { - const props = event.properties as { messageID: string; partID: string; field: string; delta: string } - const parts = input.store.part[props.messageID] - if (!parts) break - const result = Binary.search(parts, props.partID, (p) => p.id) - if (!result.found) break - input.setStore( - "part", - props.messageID, - produce((draft) => { - const part = draft[result.index] - const field = props.field as keyof typeof part - const existing = part[field] as string | undefined - ;(part[field] as string) = (existing ?? "") + props.delta - }), - ) - break - } - case "vcs.branch.updated": { - const props = event.properties as { branch?: string } - if (input.store.vcs?.branch === props.branch) break - const next = { ...input.store.vcs, branch: props.branch } - input.setStore("vcs", next) - if (input.vcsCache) input.vcsCache.setStore("value", next) - break - } - case "permission.asked": { - const permission = event.properties as PermissionRequest - const permissions = input.store.permission[permission.sessionID] - if (!permissions) { - input.setStore("permission", permission.sessionID, [permission]) - break - } - const result = Binary.search(permissions, permission.id, (p) => p.id) - if (result.found) { - input.setStore("permission", permission.sessionID, result.index, reconcile(permission)) - break - } - input.setStore( - "permission", - permission.sessionID, - produce((draft) => { - draft.splice(result.index, 0, permission) - }), - ) - break - } - case "permission.replied": { - const props = event.properties as { sessionID: string; requestID: string } - const permissions = input.store.permission[props.sessionID] - if (!permissions) break - const result = Binary.search(permissions, props.requestID, (p) => p.id) - if (!result.found) break - input.setStore( - "permission", - props.sessionID, - produce((draft) => { - draft.splice(result.index, 1) - }), - ) - break - } - case "question.asked": { - const question = event.properties as QuestionRequest - const questions = input.store.question[question.sessionID] - if (!questions) { - input.setStore("question", question.sessionID, [question]) - break - } - const result = Binary.search(questions, question.id, (q) => q.id) - if (result.found) { - input.setStore("question", question.sessionID, result.index, reconcile(question)) - break - } - input.setStore( - "question", - question.sessionID, - produce((draft) => { - draft.splice(result.index, 0, question) - }), - ) - break - } - case "question.replied": - case "question.rejected": { - const props = event.properties as { sessionID: string; requestID: string } - const questions = input.store.question[props.sessionID] - if (!questions) break - const result = Binary.search(questions, props.requestID, (q) => q.id) - if (!result.found) break - input.setStore( - "question", - props.sessionID, - produce((draft) => { - draft.splice(result.index, 1) - }), - ) - break - } - case "lsp.updated": { - input.loadLsp() - break - } - } -} diff --git a/packages/app/src/context/global-sync/eviction.ts b/packages/app/src/context/global-sync/eviction.ts deleted file mode 100644 index 676a6ee17e1..00000000000 --- a/packages/app/src/context/global-sync/eviction.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { DisposeCheck, EvictPlan } from "./types" - -export function pickDirectoriesToEvict(input: EvictPlan) { - const overflow = Math.max(0, input.stores.length - input.max) - let pendingOverflow = overflow - const sorted = input.stores - .filter((dir) => !input.pins.has(dir)) - .slice() - .sort((a, b) => (input.state.get(a)?.lastAccessAt ?? 0) - (input.state.get(b)?.lastAccessAt ?? 0)) - const output: string[] = [] - for (const dir of sorted) { - const last = input.state.get(dir)?.lastAccessAt ?? 0 - const idle = input.now - last >= input.ttl - if (!idle && pendingOverflow <= 0) continue - output.push(dir) - if (pendingOverflow > 0) pendingOverflow -= 1 - } - return output -} - -export function canDisposeDirectory(input: DisposeCheck) { - if (!input.directory) return false - if (!input.hasStore) return false - if (input.pinned) return false - if (input.booting) return false - if (input.loadingSessions) return false - return true -} diff --git a/packages/app/src/context/global-sync/queue.ts b/packages/app/src/context/global-sync/queue.ts deleted file mode 100644 index 5c228dac043..00000000000 --- a/packages/app/src/context/global-sync/queue.ts +++ /dev/null @@ -1,84 +0,0 @@ -type QueueInput = { - paused: () => boolean - bootstrap: () => Promise - bootstrapInstance: (directory: string) => Promise | void -} - -export function createRefreshQueue(input: QueueInput) { - const queued = new Set() - let root = false - let running = false - let timer: ReturnType | undefined - - const tick = () => new Promise((resolve) => setTimeout(resolve, 0)) - - const take = (count: number) => { - if (queued.size === 0) return [] as string[] - const items: string[] = [] - for (const item of queued) { - queued.delete(item) - items.push(item) - if (items.length >= count) break - } - return items - } - - const schedule = () => { - if (timer) return - timer = setTimeout(() => { - timer = undefined - void drain() - }, 0) - } - - const push = (directory: string) => { - if (!directory) return - queued.add(directory) - if (input.paused()) return - schedule() - } - - const refresh = () => { - root = true - if (input.paused()) return - schedule() - } - - async function drain() { - if (running) return - running = true - try { - while (true) { - if (input.paused()) return - if (root) { - root = false - await input.bootstrap() - await tick() - continue - } - const dirs = take(2) - if (dirs.length === 0) return - await Promise.all(dirs.map((dir) => input.bootstrapInstance(dir))) - await tick() - } - } finally { - running = false - // oxlint-disable-next-line no-unsafe-finally -- intentional: early return skips schedule() when paused - if (input.paused()) return - if (root || queued.size) schedule() - } - } - - return { - push, - refresh, - clear(directory: string) { - queued.delete(directory) - }, - dispose() { - if (!timer) return - clearTimeout(timer) - timer = undefined - }, - } -} diff --git a/packages/app/src/context/global-sync/session-cache.test.ts b/packages/app/src/context/global-sync/session-cache.test.ts deleted file mode 100644 index 471c632d802..00000000000 --- a/packages/app/src/context/global-sync/session-cache.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { - Message, - Part, - PermissionRequest, - QuestionRequest, - SessionStatus, - SnapshotFileDiff, - Todo, -} from "@kilocode/sdk/v2/client" -import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache" - -const msg = (id: string, sessionID: string) => - ({ - id, - sessionID, - role: "user", - time: { created: 1 }, - agent: "assistant", - model: { providerID: "openai", modelID: "gpt" }, - }) as Message - -const part = (id: string, sessionID: string, messageID: string) => - ({ - id, - sessionID, - messageID, - type: "text", - text: id, - }) as Part - -describe("app session cache", () => { - test("dropSessionCaches clears orphaned parts without message rows", () => { - const store: { - session_status: Record - session_diff: Record - todo: Record - message: Record - part: Record - permission: Record - question: Record - } = { - session_status: { ses_1: { type: "busy" } as SessionStatus }, - session_diff: { ses_1: [] }, - todo: { ses_1: [] as Todo[] }, - message: {}, - part: { msg_1: [part("prt_1", "ses_1", "msg_1")] }, - permission: { ses_1: [] as PermissionRequest[] }, - question: { ses_1: [] as QuestionRequest[] }, - } - - dropSessionCaches(store, ["ses_1"]) - - expect(store.message.ses_1).toBeUndefined() - expect(store.part.msg_1).toBeUndefined() - expect(store.todo.ses_1).toBeUndefined() - expect(store.session_diff.ses_1).toBeUndefined() - expect(store.session_status.ses_1).toBeUndefined() - expect(store.permission.ses_1).toBeUndefined() - expect(store.question.ses_1).toBeUndefined() - }) - - test("dropSessionCaches clears message-backed parts", () => { - const m = msg("msg_1", "ses_1") - const store: { - session_status: Record - session_diff: Record - todo: Record - message: Record - part: Record - permission: Record - question: Record - } = { - session_status: {}, - session_diff: {}, - todo: {}, - message: { ses_1: [m] }, - part: { [m.id]: [part("prt_1", "ses_1", m.id)] }, - permission: {}, - question: {}, - } - - dropSessionCaches(store, ["ses_1"]) - - expect(store.message.ses_1).toBeUndefined() - expect(store.part[m.id]).toBeUndefined() - }) - - test("pickSessionCacheEvictions preserves requested sessions", () => { - const seen = new Set(["ses_1", "ses_2", "ses_3"]) - - const stale = pickSessionCacheEvictions({ - seen, - keep: "ses_4", - limit: 2, - preserve: ["ses_1"], - }) - - expect(stale).toEqual(["ses_2", "ses_3"]) - expect([...seen]).toEqual(["ses_1", "ses_4"]) - }) -}) diff --git a/packages/app/src/context/global-sync/session-cache.ts b/packages/app/src/context/global-sync/session-cache.ts deleted file mode 100644 index b578997307c..00000000000 --- a/packages/app/src/context/global-sync/session-cache.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { - Message, - Part, - PermissionRequest, - QuestionRequest, - SessionStatus, - SnapshotFileDiff, - Todo, -} from "@kilocode/sdk/v2/client" - -export const SESSION_CACHE_LIMIT = 40 - -type SessionCache = { - session_status: Record - session_diff: Record - todo: Record - message: Record - part: Record - permission: Record - question: Record -} - -export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable) { - const stale = new Set(Array.from(sessionIDs).filter(Boolean)) - if (stale.size === 0) return - - for (const key of Object.keys(store.part)) { - const parts = store.part[key] - if (!parts?.some((part) => stale.has(part?.sessionID ?? ""))) continue - delete store.part[key] - } - - for (const sessionID of stale) { - delete store.message[sessionID] - delete store.todo[sessionID] - delete store.session_diff[sessionID] - delete store.session_status[sessionID] - delete store.permission[sessionID] - delete store.question[sessionID] - } -} - -export function pickSessionCacheEvictions(input: { - seen: Set - keep: string - limit: number - preserve?: Iterable -}) { - const stale: string[] = [] - const keep = new Set([input.keep, ...Array.from(input.preserve ?? [])]) - if (input.seen.has(input.keep)) input.seen.delete(input.keep) - input.seen.add(input.keep) - for (const id of input.seen) { - if (input.seen.size - stale.length <= input.limit) break - if (keep.has(id)) continue - stale.push(id) - } - for (const id of stale) { - input.seen.delete(id) - } - return stale -} diff --git a/packages/app/src/context/global-sync/session-load.ts b/packages/app/src/context/global-sync/session-load.ts deleted file mode 100644 index 3693dcb460d..00000000000 --- a/packages/app/src/context/global-sync/session-load.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { RootLoadArgs } from "./types" - -export async function loadRootSessionsWithFallback(input: RootLoadArgs) { - try { - const result = await input.list({ directory: input.directory, roots: true, limit: input.limit }) - return { - data: result.data, - limit: input.limit, - limited: true, - } as const - } catch { - const result = await input.list({ directory: input.directory, roots: true }) - return { - data: result.data, - limit: input.limit, - limited: false, - } as const - } -} - -export function estimateRootSessionTotal(input: { count: number; limit: number; limited: boolean }) { - if (!input.limited) return input.count - if (input.count < input.limit) return input.count - return input.count + 1 -} diff --git a/packages/app/src/context/global-sync/session-prefetch.test.ts b/packages/app/src/context/global-sync/session-prefetch.test.ts deleted file mode 100644 index 1ce83346987..00000000000 --- a/packages/app/src/context/global-sync/session-prefetch.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { - clearSessionPrefetch, - clearSessionPrefetchDirectory, - getSessionPrefetch, - runSessionPrefetch, - setSessionPrefetch, - shouldSkipSessionPrefetch, -} from "./session-prefetch" - -describe("session prefetch", () => { - test("stores and clears message metadata by directory", () => { - clearSessionPrefetch("/tmp/a", ["ses_1"]) - clearSessionPrefetch("/tmp/b", ["ses_1"]) - - setSessionPrefetch({ - directory: "/tmp/a", - sessionID: "ses_1", - limit: 200, - cursor: "abc", - complete: false, - at: 123, - }) - - expect(getSessionPrefetch("/tmp/a", "ses_1")).toEqual({ limit: 200, cursor: "abc", complete: false, at: 123 }) - expect(getSessionPrefetch("/tmp/b", "ses_1")).toBeUndefined() - - clearSessionPrefetch("/tmp/a", ["ses_1"]) - - expect(getSessionPrefetch("/tmp/a", "ses_1")).toBeUndefined() - }) - - test("dedupes inflight work", async () => { - clearSessionPrefetch("/tmp/c", ["ses_2"]) - - let calls = 0 - const run = () => - runSessionPrefetch({ - directory: "/tmp/c", - sessionID: "ses_2", - task: async () => { - calls += 1 - return { limit: 100, cursor: "next", complete: true, at: 456 } - }, - }) - - const [a, b] = await Promise.all([run(), run()]) - - expect(calls).toBe(1) - expect(a).toEqual({ limit: 100, cursor: "next", complete: true, at: 456 }) - expect(b).toEqual({ limit: 100, cursor: "next", complete: true, at: 456 }) - }) - - test("clears a whole directory", () => { - setSessionPrefetch({ directory: "/tmp/d", sessionID: "ses_1", limit: 10, cursor: "a", complete: true, at: 1 }) - setSessionPrefetch({ directory: "/tmp/d", sessionID: "ses_2", limit: 20, cursor: "b", complete: false, at: 2 }) - setSessionPrefetch({ directory: "/tmp/e", sessionID: "ses_1", limit: 30, cursor: "c", complete: true, at: 3 }) - - clearSessionPrefetchDirectory("/tmp/d") - - expect(getSessionPrefetch("/tmp/d", "ses_1")).toBeUndefined() - expect(getSessionPrefetch("/tmp/d", "ses_2")).toBeUndefined() - expect(getSessionPrefetch("/tmp/e", "ses_1")).toEqual({ limit: 30, cursor: "c", complete: true, at: 3 }) - }) - - test("refreshes stale first-page prefetched history", () => { - expect( - shouldSkipSessionPrefetch({ - message: true, - info: { limit: 200, cursor: "x", complete: false, at: 1 }, - chunk: 200, - now: 1 + 15_001, - }), - ).toBe(false) - }) - - test("keeps deeper or complete history cached", () => { - expect( - shouldSkipSessionPrefetch({ - message: true, - info: { limit: 400, cursor: "x", complete: false, at: 1 }, - chunk: 200, - now: 1 + 15_001, - }), - ).toBe(true) - - expect( - shouldSkipSessionPrefetch({ - message: true, - info: { limit: 120, complete: true, at: 1 }, - chunk: 200, - now: 1 + 15_001, - }), - ).toBe(true) - }) -}) diff --git a/packages/app/src/context/global-sync/session-prefetch.ts b/packages/app/src/context/global-sync/session-prefetch.ts deleted file mode 100644 index 608561f855c..00000000000 --- a/packages/app/src/context/global-sync/session-prefetch.ts +++ /dev/null @@ -1,100 +0,0 @@ -const key = (directory: string, sessionID: string) => `${directory}\n${sessionID}` - -export const SESSION_PREFETCH_TTL = 15_000 - -type Meta = { - limit: number - cursor?: string - complete: boolean - at: number -} - -export function shouldSkipSessionPrefetch(input: { message: boolean; info?: Meta; chunk: number; now?: number }) { - if (input.message) { - if (!input.info) return true - if (input.info.complete) return true - if (input.info.limit > input.chunk) return true - } else { - if (!input.info) return false - } - - return (input.now ?? Date.now()) - input.info.at < SESSION_PREFETCH_TTL -} - -const cache = new Map() -const inflight = new Map>() -const rev = new Map() - -const version = (id: string) => rev.get(id) ?? 0 - -export function getSessionPrefetch(directory: string, sessionID: string) { - return cache.get(key(directory, sessionID)) -} - -export function getSessionPrefetchPromise(directory: string, sessionID: string) { - return inflight.get(key(directory, sessionID)) -} - -export function clearSessionPrefetchInflight() { - inflight.clear() -} - -export function isSessionPrefetchCurrent(directory: string, sessionID: string, value: number) { - return version(key(directory, sessionID)) === value -} - -export function runSessionPrefetch(input: { - directory: string - sessionID: string - task: (value: number) => Promise -}) { - const id = key(input.directory, input.sessionID) - const pending = inflight.get(id) - if (pending) return pending - - const value = version(id) - - const promise = input.task(value).finally(() => { - if (inflight.get(id) === promise) inflight.delete(id) - }) - - inflight.set(id, promise) - return promise -} - -export function setSessionPrefetch(input: { - directory: string - sessionID: string - limit: number - cursor?: string - complete: boolean - at?: number -}) { - cache.set(key(input.directory, input.sessionID), { - limit: input.limit, - cursor: input.cursor, - complete: input.complete, - at: input.at ?? Date.now(), - }) -} - -export function clearSessionPrefetch(directory: string, sessionIDs: Iterable) { - for (const sessionID of sessionIDs) { - if (!sessionID) continue - const id = key(directory, sessionID) - rev.set(id, version(id) + 1) - cache.delete(id) - inflight.delete(id) - } -} - -export function clearSessionPrefetchDirectory(directory: string) { - const prefix = `${directory}\n` - const keys = new Set([...cache.keys(), ...inflight.keys()]) - for (const id of keys) { - if (!id.startsWith(prefix)) continue - rev.set(id, version(id) + 1) - cache.delete(id) - inflight.delete(id) - } -} diff --git a/packages/app/src/context/global-sync/session-trim.test.ts b/packages/app/src/context/global-sync/session-trim.test.ts deleted file mode 100644 index da9ef3498f5..00000000000 --- a/packages/app/src/context/global-sync/session-trim.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { PermissionRequest, Session } from "@kilocode/sdk/v2/client" -import { trimSessions } from "./session-trim" - -const session = (input: { id: string; parentID?: string; created: number; updated?: number; archived?: number }) => - ({ - id: input.id, - parentID: input.parentID, - time: { - created: input.created, - updated: input.updated, - archived: input.archived, - }, - }) as Session - -describe("trimSessions", () => { - test("keeps base roots and recent roots beyond the limit", () => { - const now = 1_000_000 - const list = [ - session({ id: "a", created: now - 100_000 }), - session({ id: "b", created: now - 90_000 }), - session({ id: "c", created: now - 80_000 }), - session({ id: "d", created: now - 70_000, updated: now - 1_000 }), - session({ id: "e", created: now - 60_000, archived: now - 10 }), - ] - - const result = trimSessions(list, { limit: 2, permission: {}, now }) - expect(result.map((x) => x.id)).toEqual(["a", "b", "c", "d"]) - }) - - test("keeps children when root is kept, permission exists, or child is recent", () => { - const now = 1_000_000 - const list = [ - session({ id: "root-1", created: now - 1000 }), - session({ id: "root-2", created: now - 2000 }), - session({ id: "z-root", created: now - 30_000_000 }), - session({ id: "child-kept-by-root", parentID: "root-1", created: now - 20_000_000 }), - session({ id: "child-kept-by-permission", parentID: "z-root", created: now - 20_000_000 }), - session({ id: "child-kept-by-recency", parentID: "z-root", created: now - 500 }), - session({ id: "child-trimmed", parentID: "z-root", created: now - 20_000_000 }), - ] - - const result = trimSessions(list, { - limit: 2, - permission: { - "child-kept-by-permission": [{ id: "perm-1" } as PermissionRequest], - }, - now, - }) - - expect(result.map((x) => x.id)).toEqual([ - "child-kept-by-permission", - "child-kept-by-recency", - "child-kept-by-root", - "root-1", - "root-2", - ]) - }) -}) diff --git a/packages/app/src/context/global-sync/session-trim.ts b/packages/app/src/context/global-sync/session-trim.ts deleted file mode 100644 index 8aa31400217..00000000000 --- a/packages/app/src/context/global-sync/session-trim.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { PermissionRequest, Session } from "@kilocode/sdk/v2/client" -import { cmp } from "./utils" -import { SESSION_RECENT_LIMIT, SESSION_RECENT_WINDOW } from "./types" - -export function sessionUpdatedAt(session: Session) { - return session.time.updated ?? session.time.created -} - -export function compareSessionRecent(a: Session, b: Session) { - const aUpdated = sessionUpdatedAt(a) - const bUpdated = sessionUpdatedAt(b) - if (aUpdated !== bUpdated) return bUpdated - aUpdated - return cmp(a.id, b.id) -} - -export function takeRecentSessions(sessions: Session[], limit: number, cutoff: number) { - if (limit <= 0) return [] as Session[] - const selected: Session[] = [] - const seen = new Set() - for (const session of sessions) { - if (!session?.id) continue - if (seen.has(session.id)) continue - seen.add(session.id) - if (sessionUpdatedAt(session) <= cutoff) continue - const index = selected.findIndex((x) => compareSessionRecent(session, x) < 0) - if (index === -1) selected.push(session) - if (index !== -1) selected.splice(index, 0, session) - if (selected.length > limit) selected.pop() - } - return selected -} - -export function trimSessions( - input: Session[], - options: { limit: number; permission: Record; now?: number }, -) { - const limit = Math.max(0, options.limit) - const cutoff = (options.now ?? Date.now()) - SESSION_RECENT_WINDOW - const all = input - .filter((s) => !!s?.id) - .filter((s) => !s.time?.archived) - .sort((a, b) => cmp(a.id, b.id)) - const roots = all.filter((s) => !s.parentID) - const children = all.filter((s) => !!s.parentID) - const base = roots.slice(0, limit) - const recent = takeRecentSessions(roots.slice(limit), SESSION_RECENT_LIMIT, cutoff) - const keepRoots = [...base, ...recent] - const keepRootIds = new Set(keepRoots.map((s) => s.id)) - const keepChildren = children.filter((s) => { - if (s.parentID && keepRootIds.has(s.parentID)) return true - const perms = options.permission[s.id] ?? [] - if (perms.length > 0) return true - return sessionUpdatedAt(s) > cutoff - }) - return [...keepRoots, ...keepChildren].sort((a, b) => cmp(a.id, b.id)) -} diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts deleted file mode 100644 index a1a51842684..00000000000 --- a/packages/app/src/context/global-sync/types.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { - Agent, - Command, - Config, - LspStatus, - McpStatus, - Message, - Part, - Path, - PermissionRequest, - ProviderListResponse, - QuestionRequest, - Session, - SessionStatus, - SnapshotFileDiff, - Todo, - VcsInfo, -} from "@kilocode/sdk/v2/client" -import type { Accessor } from "solid-js" -import type { SetStoreFunction, Store } from "solid-js/store" - -export type ProjectMeta = { - name?: string - icon?: { - override?: string - color?: string - } - commands?: { - start?: string - } -} - -export type State = { - status: "loading" | "partial" | "complete" - agent: Agent[] - command: Command[] - project: string - projectMeta: ProjectMeta | undefined - icon: string | undefined - provider_ready: boolean - provider: ProviderListResponse - config: Config - path: Path - session: Session[] - sessionTotal: number - session_status: { - [sessionID: string]: SessionStatus - } - session_diff: { - [sessionID: string]: SnapshotFileDiff[] - } - todo: { - [sessionID: string]: Todo[] - } - permission: { - [sessionID: string]: PermissionRequest[] - } - question: { - [sessionID: string]: QuestionRequest[] - } - mcp_ready: boolean - mcp: { - [name: string]: McpStatus - } - lsp_ready: boolean - lsp: LspStatus[] - vcs: VcsInfo | undefined - limit: number - message: { - [sessionID: string]: Message[] - } - part: { - [messageID: string]: Part[] - } -} - -export type VcsCache = { - store: Store<{ value: VcsInfo | undefined }> - setStore: SetStoreFunction<{ value: VcsInfo | undefined }> - ready: Accessor -} - -export type MetaCache = { - store: Store<{ value: ProjectMeta | undefined }> - setStore: SetStoreFunction<{ value: ProjectMeta | undefined }> - ready: Accessor -} - -export type IconCache = { - store: Store<{ value: string | undefined }> - setStore: SetStoreFunction<{ value: string | undefined }> - ready: Accessor -} - -export type ChildOptions = { - bootstrap?: boolean -} - -export type DirState = { - lastAccessAt: number -} - -export type EvictPlan = { - stores: string[] - state: Map - pins: Set - max: number - ttl: number - now: number -} - -export type DisposeCheck = { - directory: string - hasStore: boolean - pinned: boolean - booting: boolean - loadingSessions: boolean -} - -export type RootLoadArgs = { - directory: string - limit: number - list: (query: { directory: string; roots: true; limit?: number }) => Promise<{ data?: Session[] }> -} - -export type RootLoadResult = { - data?: Session[] - limit: number - limited: boolean -} - -export const MAX_DIR_STORES = 30 -export const DIR_IDLE_TTL_MS = 20 * 60 * 1000 -export const SESSION_RECENT_WINDOW = 4 * 60 * 60 * 1000 -export const SESSION_RECENT_LIMIT = 50 diff --git a/packages/app/src/context/global-sync/utils.test.ts b/packages/app/src/context/global-sync/utils.test.ts deleted file mode 100644 index 24f1547c27f..00000000000 --- a/packages/app/src/context/global-sync/utils.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { Agent } from "@kilocode/sdk/v2/client" -import { normalizeAgentList } from "./utils" - -const agent = (name = "build") => - ({ - name, - mode: "primary", - permission: {}, - options: {}, - }) as Agent - -describe("normalizeAgentList", () => { - test("keeps array payloads", () => { - expect(normalizeAgentList([agent("build"), agent("docs")])).toEqual([agent("build"), agent("docs")]) - }) - - test("wraps a single agent payload", () => { - expect(normalizeAgentList(agent("docs"))).toEqual([agent("docs")]) - }) - - test("extracts agents from keyed objects", () => { - expect( - normalizeAgentList({ - build: agent("build"), - docs: agent("docs"), - }), - ).toEqual([agent("build"), agent("docs")]) - }) - - test("drops invalid payloads", () => { - expect(normalizeAgentList({ name: "AbortError" })).toEqual([]) - expect(normalizeAgentList([{ name: "build" }, agent("docs")])).toEqual([agent("docs")]) - }) -}) diff --git a/packages/app/src/context/global-sync/utils.ts b/packages/app/src/context/global-sync/utils.ts deleted file mode 100644 index e6fd6f64ba2..00000000000 --- a/packages/app/src/context/global-sync/utils.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Agent, Project, ProviderListResponse } from "@kilocode/sdk/v2/client" - -export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) - -function isAgent(input: unknown): input is Agent { - if (!input || typeof input !== "object") return false - const item = input as { name?: unknown; mode?: unknown } - if (typeof item.name !== "string") return false - return item.mode === "subagent" || item.mode === "primary" || item.mode === "all" -} - -export function normalizeAgentList(input: unknown): Agent[] { - if (Array.isArray(input)) return input.filter(isAgent) - if (isAgent(input)) return [input] - if (!input || typeof input !== "object") return [] - return Object.values(input).filter(isAgent) -} - -export function normalizeProviderList(input: ProviderListResponse): ProviderListResponse { - return { - ...input, - all: input.all.map((provider) => ({ - ...provider, - models: Object.fromEntries(Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated")), - })), - } -} - -export function sanitizeProject(project: Project) { - if (!project.icon?.url && !project.icon?.override) return project - return { - ...project, - icon: { - ...project.icon, - url: undefined, - override: undefined, - }, - } -} diff --git a/packages/app/src/context/highlights.tsx b/packages/app/src/context/highlights.tsx deleted file mode 100644 index 11ee613e643..00000000000 --- a/packages/app/src/context/highlights.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { createEffect, onCleanup } from "solid-js" -import { createStore } from "solid-js/store" -import { createSimpleContext } from "@opencode-ai/ui/context" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { usePlatform } from "@/context/platform" -import { useSettings } from "@/context/settings" -import { persisted } from "@/utils/persist" -import { DialogReleaseNotes, type Highlight } from "@/components/dialog-release-notes" - -const CHANGELOG_URL = "https://kilo.ai/changelog.json" - -type Store = { - version?: string -} - -type ParsedRelease = { - tag?: string - highlights: Highlight[] -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value) -} - -function getText(value: unknown): string | undefined { - if (typeof value === "string") { - const text = value.trim() - return text.length > 0 ? text : undefined - } - - if (typeof value === "number") return String(value) - return -} - -function normalizeVersion(value: string | undefined) { - const text = value?.trim() - if (!text) return - return text.startsWith("v") || text.startsWith("V") ? text.slice(1) : text -} - -function parseMedia(value: unknown, alt: string): Highlight["media"] | undefined { - if (!isRecord(value)) return - const type = getText(value.type)?.toLowerCase() - const src = getText(value.src) ?? getText(value.url) - if (!src) return - if (type !== "image" && type !== "video") return - - return { type, src, alt } -} - -function parseHighlight(value: unknown): Highlight | undefined { - if (!isRecord(value)) return - - const title = getText(value.title) - if (!title) return - - const description = getText(value.description) ?? getText(value.shortDescription) - if (!description) return - - const media = parseMedia(value.media, title) - return { title, description, media } -} - -function parseRelease(value: unknown): ParsedRelease | undefined { - if (!isRecord(value)) return - const tag = getText(value.tag) ?? getText(value.tag_name) ?? getText(value.name) - - if (!Array.isArray(value.highlights)) { - return { tag, highlights: [] } - } - - const highlights = value.highlights.flatMap((group) => { - if (!isRecord(group)) return [] - - const source = getText(group.source) - if (!source) return [] - if (!source.toLowerCase().includes("desktop")) return [] - - if (Array.isArray(group.items)) { - return group.items.map((item) => parseHighlight(item)).filter((item): item is Highlight => item !== undefined) - } - - const item = parseHighlight(group) - if (!item) return [] - return [item] - }) - - return { tag, highlights } -} - -function parseChangelog(value: unknown): ParsedRelease[] | undefined { - if (Array.isArray(value)) { - return value.map(parseRelease).filter((release): release is ParsedRelease => release !== undefined) - } - - if (!isRecord(value)) return - if (!Array.isArray(value.releases)) return - - return value.releases.map(parseRelease).filter((release): release is ParsedRelease => release !== undefined) -} - -function sliceHighlights(input: { releases: ParsedRelease[]; current?: string; previous?: string }) { - const current = normalizeVersion(input.current) - const previous = normalizeVersion(input.previous) - const releases = input.releases - - const start = (() => { - if (!current) return 0 - const index = releases.findIndex((release) => normalizeVersion(release.tag) === current) - return index === -1 ? 0 : index - })() - - const end = (() => { - if (!previous) return releases.length - const index = releases.findIndex((release, i) => i >= start && normalizeVersion(release.tag) === previous) - return index === -1 ? releases.length : index - })() - - const highlights = releases.slice(start, end).flatMap((release) => release.highlights) - const seen = new Set() - const unique = highlights.filter((highlight) => { - const key = dedupeKey(highlight) - if (seen.has(key)) return false - seen.add(key) - return true - }) - return unique.slice(0, 5) -} - -function dedupeKey(highlight: Highlight) { - return [highlight.title, highlight.description, highlight.media?.type ?? "", highlight.media?.src ?? ""].join("\n") -} - -function loadReleaseHighlights(value: unknown, current?: string, previous?: string) { - const releases = parseChangelog(value) - if (!releases?.length) return [] - return sliceHighlights({ releases, current, previous }) -} - -export const { use: useHighlights, provider: HighlightsProvider } = createSimpleContext({ - name: "Highlights", - gate: false, - init: () => { - const platform = usePlatform() - const dialog = useDialog() - const settings = useSettings() - const [store, setStore, _, ready] = persisted("highlights.v1", createStore({ version: undefined })) - - const [range, setRange] = createStore({ - from: undefined as string | undefined, - to: undefined as string | undefined, - }) - const state = { started: false } - let timer: ReturnType | undefined - - const clearTimer = () => { - if (timer === undefined) return - clearTimeout(timer) - timer = undefined - } - - const markSeen = () => { - if (!platform.version) return - setStore("version", platform.version) - } - - const start = (previous: string) => { - if (!settings.general.releaseNotes()) { - markSeen() - return - } - - const fetcher = platform.fetch ?? fetch - const controller = new AbortController() - onCleanup(() => { - controller.abort() - clearTimer() - }) - - fetcher(CHANGELOG_URL, { - signal: controller.signal, - headers: { Accept: "application/json" }, - }) - .then((response) => (response.ok ? (response.json() as Promise) : undefined)) - .then((json) => { - if (!json) return - const highlights = loadReleaseHighlights(json, platform.version, previous) - if (controller.signal.aborted) return - - if (highlights.length === 0) { - markSeen() - return - } - - timer = setTimeout(() => { - timer = undefined - markSeen() - dialog.show(() => ) - }, 500) - }) - .catch(() => undefined) - } - - createEffect(() => { - if (state.started) return - if (!ready()) return - if (!settings.ready()) return - if (!platform.version) return - state.started = true - - const previous = store.version - if (!previous) { - setStore("version", platform.version) - return - } - - if (previous === platform.version) return - - setRange({ from: previous, to: platform.version }) - start(previous) - }) - - return { - ready, - from: () => range.from, - to: () => range.to, - get last() { - return store.version - }, - markSeen, - } - }, -}) diff --git a/packages/app/src/context/language.tsx b/packages/app/src/context/language.tsx deleted file mode 100644 index f4cf0877d29..00000000000 --- a/packages/app/src/context/language.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import * as i18n from "@solid-primitives/i18n" -import { createEffect, createMemo, createResource } from "solid-js" -import { createStore } from "solid-js/store" -import { createSimpleContext } from "@opencode-ai/ui/context" -import { Persist, persisted } from "@/utils/persist" -import { dict as en } from "@/i18n/en" -import { dict as uiEn } from "@opencode-ai/ui/i18n/en" - -// kilocode_change start -import { dict as kiloEn } from "@kilocode/kilo-i18n/en" -// kilocode_change end - -export type Locale = - | "en" - | "zh" - | "zht" - | "ko" - | "de" - | "es" - | "fr" - | "da" - | "ja" - | "pl" - | "ru" - | "ar" - | "no" - | "br" - | "th" - | "bs" - | "nl" - | "tr" - -type RawDictionary = typeof en & typeof uiEn & typeof kiloEn // kilocode_change -type Dictionary = i18n.Flatten -type Source = { dict: Record } - -function cookie(locale: Locale) { - return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax` -} - -const LOCALES: readonly Locale[] = [ - "en", - "zh", - "zht", - "ko", - "de", - "es", - "fr", - "da", - "ja", - "pl", - "ru", - "bs", - "nl", - "ar", - "no", - "br", - "th", - "tr", -] - -const INTL: Record = { - en: "en", - zh: "zh-Hans", - zht: "zh-Hant", - ko: "ko", - de: "de", - es: "es", - fr: "fr", - da: "da", - ja: "ja", - pl: "pl", - ru: "ru", - ar: "ar", - no: "nb-NO", - br: "pt-BR", - th: "th", - bs: "bs", - nl: "nl", - - tr: "tr", -} - -const LABEL_KEY: Record = { - en: "language.en", - zh: "language.zh", - zht: "language.zht", - ko: "language.ko", - de: "language.de", - es: "language.es", - fr: "language.fr", - da: "language.da", - ja: "language.ja", - pl: "language.pl", - ru: "language.ru", - ar: "language.ar", - no: "language.no", - br: "language.br", - th: "language.th", - bs: "language.bs", - nl: "language.nl", - tr: "language.tr", -} - -const base = i18n.flatten({ ...en, ...uiEn, ...kiloEn }) as Dictionary // kilocode_change -const dicts = new Map([["en", base]]) - -const merge = (app: Promise, ui: Promise) => - Promise.all([app, ui]).then(([a, b]) => ({ ...base, ...i18n.flatten({ ...a.dict, ...b.dict }) }) as Dictionary) - -const loaders: Record, () => Promise> = { - zh: () => merge(import("@/i18n/zh"), import("@opencode-ai/ui/i18n/zh")), - zht: () => merge(import("@/i18n/zht"), import("@opencode-ai/ui/i18n/zht")), - ko: () => merge(import("@/i18n/ko"), import("@opencode-ai/ui/i18n/ko")), - de: () => merge(import("@/i18n/de"), import("@opencode-ai/ui/i18n/de")), - es: () => merge(import("@/i18n/es"), import("@opencode-ai/ui/i18n/es")), - fr: () => merge(import("@/i18n/fr"), import("@opencode-ai/ui/i18n/fr")), - da: () => merge(import("@/i18n/da"), import("@opencode-ai/ui/i18n/da")), - ja: () => merge(import("@/i18n/ja"), import("@opencode-ai/ui/i18n/ja")), - pl: () => merge(import("@/i18n/pl"), import("@opencode-ai/ui/i18n/pl")), - ru: () => merge(import("@/i18n/ru"), import("@opencode-ai/ui/i18n/ru")), - ar: () => merge(import("@/i18n/ar"), import("@opencode-ai/ui/i18n/ar")), - no: () => merge(import("@/i18n/no"), import("@opencode-ai/ui/i18n/no")), - br: () => merge(import("@/i18n/br"), import("@opencode-ai/ui/i18n/br")), - th: () => merge(import("@/i18n/th"), import("@opencode-ai/ui/i18n/th")), - bs: () => merge(import("@/i18n/bs"), import("@opencode-ai/ui/i18n/bs")), - nl: () => merge(import("@/i18n/nl"), import("@opencode-ai/ui/i18n/nl")), - tr: () => merge(import("@/i18n/tr"), import("@opencode-ai/ui/i18n/tr")), -} - -function loadDict(locale: Locale) { - const hit = dicts.get(locale) - if (hit) return Promise.resolve(hit) - if (locale === "en") return Promise.resolve(base) - const load = loaders[locale] - return load().then((next: Dictionary) => { - dicts.set(locale, next) - return next - }) -} - -export function loadLocaleDict(locale: Locale) { - return loadDict(locale).then(() => undefined) -} - -const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [ - { locale: "en", match: (language) => language.startsWith("en") }, - { locale: "zht", match: (language) => language.startsWith("zh") && language.includes("hant") }, - { locale: "zh", match: (language) => language.startsWith("zh") }, - { locale: "ko", match: (language) => language.startsWith("ko") }, - { locale: "de", match: (language) => language.startsWith("de") }, - { locale: "es", match: (language) => language.startsWith("es") }, - { locale: "fr", match: (language) => language.startsWith("fr") }, - { locale: "da", match: (language) => language.startsWith("da") }, - { locale: "ja", match: (language) => language.startsWith("ja") }, - { locale: "pl", match: (language) => language.startsWith("pl") }, - { locale: "ru", match: (language) => language.startsWith("ru") }, - { locale: "ar", match: (language) => language.startsWith("ar") }, - { - locale: "no", - match: (language) => language.startsWith("no") || language.startsWith("nb") || language.startsWith("nn"), - }, - { locale: "br", match: (language) => language.startsWith("pt") }, - { locale: "th", match: (language) => language.startsWith("th") }, - { locale: "bs", match: (language) => language.startsWith("bs") }, - { locale: "nl", match: (language) => language.startsWith("nl") }, - { locale: "tr", match: (language) => language.startsWith("tr") }, -] - -function detectLocale(): Locale { - if (typeof navigator !== "object") return "en" - - const languages = navigator.languages?.length ? navigator.languages : [navigator.language] - for (const language of languages) { - if (!language) continue - const normalized = language.toLowerCase() - const match = localeMatchers.find((entry) => entry.match(normalized)) - if (match) return match.locale - } - - return "en" -} - -export function normalizeLocale(value: string): Locale { - return LOCALES.includes(value as Locale) ? (value as Locale) : "en" -} - -function readStoredLocale() { - if (typeof localStorage !== "object") return - try { - const raw = localStorage.getItem("opencode.global.dat:language") - if (!raw) return - const next = JSON.parse(raw) as { locale?: string } - if (typeof next?.locale !== "string") return - return normalizeLocale(next.locale) - } catch { - return - } -} - -const warm = readStoredLocale() ?? detectLocale() -if (warm !== "en") void loadDict(warm) - -export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({ - name: "Language", - init: (props: { locale?: Locale }) => { - const initial = props.locale ?? readStoredLocale() ?? detectLocale() - const [store, setStore, _, ready] = persisted( - Persist.global("language", ["language.v1"]), - createStore({ - locale: initial, - }), - ) - - const locale = createMemo(() => normalizeLocale(store.locale)) - const intl = createMemo(() => INTL[locale()]) - - const [dict] = createResource(locale, loadDict, { - initialValue: dicts.get(initial) ?? base, - }) - - const t = i18n.translator(() => dict() ?? base, i18n.resolveTemplate) as ( - key: keyof Dictionary, - params?: Record, - ) => string - - const label = (value: Locale) => t(LABEL_KEY[value]) - - createEffect(() => { - if (typeof document !== "object") return - document.documentElement.lang = locale() - document.cookie = cookie(locale()) - }) - - return { - ready, - locale, - intl, - locales: LOCALES, - label, - t, - setLocale(next: Locale) { - setStore("locale", normalizeLocale(next)) - }, - } - }, -}) diff --git a/packages/app/src/context/layout-scroll.test.ts b/packages/app/src/context/layout-scroll.test.ts deleted file mode 100644 index 483be150f66..00000000000 --- a/packages/app/src/context/layout-scroll.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { describe, expect, test, vi } from "bun:test" -import { createScrollPersistence } from "./layout-scroll" - -describe("createScrollPersistence", () => { - test("debounces persisted scroll writes", () => { - vi.useFakeTimers() - try { - const snapshot = { - session: { - review: { x: 0, y: 0 }, - }, - } as Record> - const writes: Array> = [] - const scroll = createScrollPersistence({ - debounceMs: 10, - getSnapshot: (sessionKey) => snapshot[sessionKey], - onFlush: (sessionKey, next) => { - snapshot[sessionKey] = next - writes.push(next) - }, - }) - - for (const i of Array.from({ length: 30 }, (_, n) => n + 1)) { - scroll.setScroll("session", "review", { x: 0, y: i }) - } - - vi.advanceTimersByTime(9) - expect(writes).toHaveLength(0) - - vi.advanceTimersByTime(1) - - expect(writes).toHaveLength(1) - expect(writes[0]?.review).toEqual({ x: 0, y: 30 }) - - scroll.setScroll("session", "review", { x: 0, y: 30 }) - vi.advanceTimersByTime(20) - - expect(writes).toHaveLength(1) - scroll.dispose() - } finally { - vi.useRealTimers() - } - }) - - test("reseeds empty cache after persisted snapshot loads", () => { - const snapshot = { - session: {}, - } as Record> - - const scroll = createScrollPersistence({ - getSnapshot: (sessionKey) => snapshot[sessionKey], - onFlush: () => {}, - }) - - expect(scroll.scroll("session", "review")).toBeUndefined() - - snapshot.session = { - review: { x: 12, y: 34 }, - } - - expect(scroll.scroll("session", "review")).toEqual({ x: 12, y: 34 }) - scroll.dispose() - }) -}) diff --git a/packages/app/src/context/layout-scroll.ts b/packages/app/src/context/layout-scroll.ts deleted file mode 100644 index ef66eccd904..00000000000 --- a/packages/app/src/context/layout-scroll.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { createStore, produce } from "solid-js/store" - -export type SessionScroll = { - x: number - y: number -} - -type ScrollMap = Record - -type Options = { - debounceMs?: number - getSnapshot: (sessionKey: string) => ScrollMap | undefined - onFlush: (sessionKey: string, scroll: ScrollMap) => void -} - -export function createScrollPersistence(opts: Options) { - const wait = opts.debounceMs ?? 200 - const [cache, setCache] = createStore>({}) - const dirty = new Set() - const timers = new Map>() - - function clone(input?: ScrollMap) { - const out: ScrollMap = {} - if (!input) return out - - for (const key of Object.keys(input)) { - const pos = input[key] - if (!pos) continue - out[key] = { x: pos.x, y: pos.y } - } - - return out - } - - function seed(sessionKey: string) { - const next = clone(opts.getSnapshot(sessionKey)) - const current = cache[sessionKey] - if (!current) { - setCache(sessionKey, next) - return - } - - if (Object.keys(current).length > 0) return - if (Object.keys(next).length === 0) return - setCache(sessionKey, next) - } - - function scroll(sessionKey: string, tab: string) { - seed(sessionKey) - return cache[sessionKey]?.[tab] ?? opts.getSnapshot(sessionKey)?.[tab] - } - - function schedule(sessionKey: string) { - const prev = timers.get(sessionKey) - if (prev) clearTimeout(prev) - timers.set( - sessionKey, - setTimeout(() => flush(sessionKey), wait), - ) - } - - function setScroll(sessionKey: string, tab: string, pos: SessionScroll) { - seed(sessionKey) - - const prev = cache[sessionKey]?.[tab] - if (prev?.x === pos.x && prev?.y === pos.y) return - - setCache(sessionKey, tab, { x: pos.x, y: pos.y }) - dirty.add(sessionKey) - schedule(sessionKey) - } - - function flush(sessionKey: string) { - const timer = timers.get(sessionKey) - if (timer) clearTimeout(timer) - timers.delete(sessionKey) - - if (!dirty.has(sessionKey)) return - dirty.delete(sessionKey) - - opts.onFlush(sessionKey, clone(cache[sessionKey])) - } - - function flushAll() { - const keys = Array.from(dirty) - if (keys.length === 0) return - - for (const key of keys) { - flush(key) - } - } - - function drop(keys: string[]) { - if (keys.length === 0) return - - for (const key of keys) { - const timer = timers.get(key) - if (timer) clearTimeout(timer) - timers.delete(key) - dirty.delete(key) - } - - setCache( - produce((draft) => { - for (const key of keys) { - delete draft[key] - } - }), - ) - } - - function dispose() { - drop(Array.from(timers.keys())) - } - - return { - cache, - drop, - flush, - flushAll, - scroll, - seed, - setScroll, - dispose, - } -} diff --git a/packages/app/src/context/layout.test.ts b/packages/app/src/context/layout.test.ts deleted file mode 100644 index 582d5edbd29..00000000000 --- a/packages/app/src/context/layout.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { createRoot, createSignal } from "solid-js" -import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout" - -describe("layout session-key helpers", () => { - test("couples touch and scroll seed in order", () => { - const calls: string[] = [] - const result = ensureSessionKey( - "dir/a", - (key) => calls.push(`touch:${key}`), - (key) => calls.push(`seed:${key}`), - ) - - expect(result).toBe("dir/a") - expect(calls).toEqual(["touch:dir/a", "seed:dir/a"]) - }) - - test("reads dynamic accessor keys lazily", () => { - const seen: string[] = [] - - createRoot((dispose) => { - const [key, setKey] = createSignal("dir/one") - const read = createSessionKeyReader(key, (value) => seen.push(value)) - - expect(read()).toBe("dir/one") - setKey("dir/two") - expect(read()).toBe("dir/two") - - dispose() - }) - - expect(seen).toEqual(["dir/one", "dir/two"]) - }) -}) - -describe("pruneSessionKeys", () => { - test("keeps active key and drops lowest-used keys", () => { - const drop = pruneSessionKeys({ - keep: "k4", - max: 3, - used: new Map([ - ["k1", 1], - ["k2", 2], - ["k3", 3], - ["k4", 4], - ]), - view: ["k1", "k2", "k4"], - tabs: ["k1", "k3", "k4"], - }) - - expect(drop).toEqual(["k1"]) - expect(drop.includes("k4")).toBe(false) - }) - - test("does not prune without keep key", () => { - const drop = pruneSessionKeys({ - keep: undefined, - max: 1, - used: new Map([ - ["k1", 1], - ["k2", 2], - ]), - view: ["k1"], - tabs: ["k2"], - }) - - expect(drop).toEqual([]) - }) -}) diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx deleted file mode 100644 index 603c3c5961c..00000000000 --- a/packages/app/src/context/layout.tsx +++ /dev/null @@ -1,921 +0,0 @@ -import { createStore, produce } from "solid-js/store" -import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js" -import { createSimpleContext } from "@opencode-ai/ui/context" -import { makeEventListener } from "@solid-primitives/event-listener" -import { useGlobalSync } from "./global-sync" -import { useGlobalSDK } from "./global-sdk" -import { useServer } from "./server" -import { usePlatform } from "./platform" -import { Project } from "@kilocode/sdk/v2" -import { Persist, persisted, removePersisted } from "@/utils/persist" -import { decode64 } from "@/utils/base64" -import { same } from "@/utils/same" -import { createScrollPersistence, type SessionScroll } from "./layout-scroll" -import { createPathHelpers } from "./file/path" - -const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const -const DEFAULT_SIDEBAR_WIDTH = 344 -const DEFAULT_FILE_TREE_WIDTH = 200 -const DEFAULT_SESSION_WIDTH = 600 -const DEFAULT_TERMINAL_HEIGHT = 280 -export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number] - -export function getAvatarColors(key?: string) { - if (key && AVATAR_COLOR_KEYS.includes(key as AvatarColorKey)) { - return { - background: `var(--avatar-background-${key})`, - foreground: `var(--avatar-text-${key})`, - } - } - return { - background: "var(--surface-info-base)", - foreground: "var(--text-base)", - } -} - -type SessionTabs = { - active?: string - all: string[] -} - -type SessionView = { - scroll: Record - reviewOpen?: string[] - pendingMessage?: string - pendingMessageAt?: number -} - -type TabHandoff = { - dir: string - id: string - at: number -} - -export type LocalProject = Partial & { worktree: string; expanded: boolean } - -export type ReviewDiffStyle = "unified" | "split" - -export function ensureSessionKey(key: string, touch: (key: string) => void, seed: (key: string) => void) { - touch(key) - seed(key) - return key -} - -export function createSessionKeyReader(sessionKey: string | Accessor, ensure: (key: string) => void) { - const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey - return () => { - const value = key() - ensure(value) - return value - } -} - -export function pruneSessionKeys(input: { - keep?: string - max: number - used: Map - view: string[] - tabs: string[] -}) { - if (!input.keep) return [] - - const keys = new Set([...input.view, ...input.tabs]) - if (keys.size <= input.max) return [] - - const score = (key: string) => { - if (key === input.keep) return Number.MAX_SAFE_INTEGER - return input.used.get(key) ?? 0 - } - - return Array.from(keys) - .sort((a, b) => score(b) - score(a)) - .slice(input.max) -} - -function nextSessionTabsForOpen(current: SessionTabs | undefined, tab: string): SessionTabs { - const all = current?.all ?? [] - if (tab === "review") return { all: all.filter((x) => x !== "review"), active: tab } - if (tab === "context") return { all: [tab, ...all.filter((x) => x !== tab)], active: tab } - if (!all.includes(tab)) return { all: [...all, tab], active: tab } - return { all, active: tab } -} - -const sessionPath = (key: string) => { - const dir = key.split("/")[0] - if (!dir) return - const root = decode64(dir) - if (!root) return - return createPathHelpers(() => root) -} - -const normalizeSessionTab = (path: ReturnType | undefined, tab: string) => { - if (!tab.startsWith("file://")) return tab - if (!path) return tab - return path.tab(tab) -} - -const normalizeSessionTabList = (path: ReturnType | undefined, all: string[]) => { - const seen = new Set() - return all.flatMap((tab) => { - const value = normalizeSessionTab(path, tab) - if (seen.has(value)) return [] - seen.add(value) - return [value] - }) -} - -const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => { - const path = sessionPath(key) - return { - all: normalizeSessionTabList(path, tabs.all), - active: tabs.active ? normalizeSessionTab(path, tabs.active) : tabs.active, - } -} - -export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({ - name: "Layout", - init: () => { - const globalSdk = useGlobalSDK() - const globalSync = useGlobalSync() - const server = useServer() - const platform = usePlatform() - - const isRecord = (value: unknown): value is Record => - typeof value === "object" && value !== null && !Array.isArray(value) - - const migrate = (value: unknown) => { - if (!isRecord(value)) return value - - const sidebar = value.sidebar - const migratedSidebar = (() => { - if (!isRecord(sidebar)) return sidebar - if (typeof sidebar.workspaces !== "boolean") return sidebar - return { - ...sidebar, - workspaces: {}, - workspacesDefault: sidebar.workspaces, - } - })() - - const review = value.review - const fileTree = value.fileTree - const migratedFileTree = (() => { - if (!isRecord(fileTree)) return fileTree - if (fileTree.tab === "changes" || fileTree.tab === "all") return fileTree - - const width = typeof fileTree.width === "number" ? fileTree.width : DEFAULT_FILE_TREE_WIDTH - return { - ...fileTree, - opened: true, - width: width === 260 ? DEFAULT_FILE_TREE_WIDTH : width, - tab: "changes", - } - })() - - const migratedReview = (() => { - if (!isRecord(review)) return review - if (typeof review.panelOpened === "boolean") return review - - const opened = isRecord(fileTree) && typeof fileTree.opened === "boolean" ? fileTree.opened : true - return { - ...review, - panelOpened: opened, - } - })() - - const sessionTabs = value.sessionTabs - const migratedSessionTabs = (() => { - if (!isRecord(sessionTabs)) return sessionTabs - - let changed = false - const next = Object.fromEntries( - Object.entries(sessionTabs).map(([key, tabs]) => { - if (!isRecord(tabs) || !Array.isArray(tabs.all)) return [key, tabs] - - const current = { - all: tabs.all.filter((tab): tab is string => typeof tab === "string"), - active: typeof tabs.active === "string" ? tabs.active : undefined, - } - const normalized = normalizeStoredSessionTabs(key, current) - if (current.all.length !== tabs.all.length) changed = true - if (!same(current.all, normalized.all) || current.active !== normalized.active) changed = true - if (tabs.active !== undefined && typeof tabs.active !== "string") changed = true - return [key, normalized] - }), - ) - - if (!changed) return sessionTabs - return next - })() - - if ( - migratedSidebar === sidebar && - migratedReview === review && - migratedFileTree === fileTree && - migratedSessionTabs === sessionTabs - ) { - return value - } - - return { - ...value, - sidebar: migratedSidebar, - review: migratedReview, - fileTree: migratedFileTree, - sessionTabs: migratedSessionTabs, - } - } - - const target = Persist.global("layout", ["layout.v6"]) - const [store, setStore, _, ready] = persisted( - { ...target, migrate }, - createStore({ - sidebar: { - opened: false, - width: DEFAULT_SIDEBAR_WIDTH, - workspaces: {} as Record, - workspacesDefault: false, - }, - terminal: { - height: DEFAULT_TERMINAL_HEIGHT, - opened: false, - }, - review: { - diffStyle: "split" as ReviewDiffStyle, - panelOpened: true, - }, - fileTree: { - opened: false, - width: DEFAULT_FILE_TREE_WIDTH, - tab: "changes" as "changes" | "all", - }, - session: { - width: DEFAULT_SESSION_WIDTH, - }, - mobileSidebar: { - opened: false, - }, - sessionTabs: {} as Record, - sessionView: {} as Record, - handoff: { - tabs: undefined as TabHandoff | undefined, - }, - }), - ) - - const MAX_SESSION_KEYS = 50 - const PENDING_MESSAGE_TTL_MS = 2 * 60 * 1000 - const usage = { - active: undefined as string | undefined, - pruned: false, - used: new Map(), - } - - const SESSION_STATE_KEYS = [ - { key: "prompt", legacy: "prompt", version: "v2" }, - { key: "terminal", legacy: "terminal", version: "v1" }, - { key: "file-view", legacy: "file", version: "v1" }, - ] as const - - const dropSessionState = (keys: string[]) => { - for (const key of keys) { - const parts = key.split("/") - const dir = parts[0] - const session = parts[1] - if (!dir) continue - - for (const entry of SESSION_STATE_KEYS) { - const target = session ? Persist.session(dir, session, entry.key) : Persist.workspace(dir, entry.key) - void removePersisted(target, platform) - - const legacyKey = `${dir}/${entry.legacy}${session ? "/" + session : ""}.${entry.version}` - void removePersisted({ key: legacyKey }, platform) - } - } - } - - function prune(keep?: string) { - const drop = pruneSessionKeys({ - keep, - max: MAX_SESSION_KEYS, - used: usage.used, - view: Object.keys(store.sessionView), - tabs: Object.keys(store.sessionTabs), - }) - if (drop.length === 0) return - - setStore( - produce((draft) => { - for (const key of drop) { - delete draft.sessionView[key] - delete draft.sessionTabs[key] - } - }), - ) - - scroll.drop(drop) - dropSessionState(drop) - - for (const key of drop) { - usage.used.delete(key) - } - } - - function touch(sessionKey: string) { - usage.active = sessionKey - usage.used.set(sessionKey, Date.now()) - - if (!ready()) return - if (usage.pruned) return - - usage.pruned = true - prune(sessionKey) - } - - const scroll = createScrollPersistence({ - debounceMs: 250, - getSnapshot: (sessionKey) => store.sessionView[sessionKey]?.scroll, - onFlush: (sessionKey, next) => { - const current = store.sessionView[sessionKey] - const keep = usage.active ?? sessionKey - if (!current) { - setStore("sessionView", sessionKey, { scroll: next }) - prune(keep) - return - } - - setStore("sessionView", sessionKey, "scroll", (prev) => ({ ...prev, ...next })) - prune(keep) - }, - }) - - const ensureKey = (key: string) => ensureSessionKey(key, touch, (sessionKey) => scroll.seed(sessionKey)) - - createEffect(() => { - if (!ready()) return - if (usage.pruned) return - const active = usage.active - if (!active) return - usage.pruned = true - prune(active) - }) - - onMount(() => { - const flush = () => batch(() => scroll.flushAll()) - const handleVisibility = () => { - if (document.visibilityState !== "hidden") return - flush() - } - - makeEventListener(window, "pagehide", flush) - makeEventListener(document, "visibilitychange", handleVisibility) - - onCleanup(() => { - scroll.dispose() - }) - }) - - const [colors, setColors] = createStore>({}) - const colorRequested = new Map() - - function pickAvailableColor(used: Set): AvatarColorKey { - const available = AVATAR_COLOR_KEYS.filter((c) => !used.has(c)) - if (available.length === 0) return AVATAR_COLOR_KEYS[Math.floor(Math.random() * AVATAR_COLOR_KEYS.length)] - return available[Math.floor(Math.random() * available.length)] - } - - function enrich(project: { worktree: string; expanded: boolean }) { - const [childStore] = globalSync.child(project.worktree, { bootstrap: false }) - const projectID = childStore.project - const metadata = projectID - ? globalSync.data.project.find((x) => x.id === projectID) - : globalSync.data.project.find((x) => x.worktree === project.worktree) - - return { ...metadata, ...project } - } - - const roots = createMemo(() => { - const map = new Map() - for (const project of globalSync.data.project) { - const sandboxes = project.sandboxes ?? [] - for (const sandbox of sandboxes) { - map.set(sandbox, project.worktree) - } - } - return map - }) - - const rootFor = (directory: string) => { - const map = roots() - if (map.size === 0) return directory - - const visited = new Set() - const chain = [directory] - - while (chain.length) { - const current = chain[chain.length - 1] - if (!current) return directory - - const next = map.get(current) - if (!next) return current - - if (visited.has(next)) return directory - visited.add(next) - chain.push(next) - } - - return directory - } - - createEffect(() => { - const projects = server.projects.list() - const seen = new Set(projects.map((project) => project.worktree)) - - batch(() => { - for (const project of projects) { - const root = rootFor(project.worktree) - if (root === project.worktree) continue - - server.projects.close(project.worktree) - - if (!seen.has(root)) { - server.projects.open(root) - seen.add(root) - } - - if (project.expanded) server.projects.expand(root) - } - }) - }) - - const enriched = createMemo(() => server.projects.list().map(enrich)) - const list = createMemo(() => { - const projects = enriched() - return projects.map((project) => { - const color = project.icon?.color ?? colors[project.worktree] - if (!color) return project - const icon = project.icon ? { ...project.icon, color } : { color } - return { ...project, icon } - }) - }) - - createEffect(() => { - const projects = enriched() - if (projects.length === 0) return - if (!globalSync.ready) return - - for (const project of projects) { - if (!project.id) continue - if (project.id === "global") continue - globalSync.project.icon(project.worktree, project.icon?.override) - } - }) - - createEffect(() => { - const projects = enriched() - if (projects.length === 0) return - - for (const project of projects) { - if (project.icon?.color) colorRequested.delete(project.worktree) - } - - const used = new Set() - for (const project of projects) { - const color = project.icon?.color ?? colors[project.worktree] - if (color) used.add(color) - } - - for (const project of projects) { - if (project.icon?.color || project.icon?.override || project.icon?.url) continue - const worktree = project.worktree - const existing = colors[worktree] - const color = existing ?? pickAvailableColor(used) - if (!existing) { - used.add(color) - setColors(worktree, color) - } - if (!project.id) continue - - const requested = colorRequested.get(worktree) - if (requested === color) continue - colorRequested.set(worktree, color) - - if (project.id === "global") { - globalSync.project.meta(worktree, { icon: { color } }) - continue - } - - void globalSdk.client.project - .update({ projectID: project.id, directory: worktree, icon: { color } }) - .catch(() => { - if (colorRequested.get(worktree) === color) colorRequested.delete(worktree) - }) - } - }) - - let sessionFrame: number | undefined - let sessionTimer: number | undefined - - onMount(() => { - sessionFrame = requestAnimationFrame(() => { - sessionFrame = undefined - sessionTimer = window.setTimeout(() => { - sessionTimer = undefined - void Promise.all( - server.projects.list().map((project) => { - return globalSync.project.loadSessions(project.worktree) - }), - ) - }, 0) - }) - }) - - onCleanup(() => { - if (sessionFrame !== undefined) cancelAnimationFrame(sessionFrame) - if (sessionTimer !== undefined) window.clearTimeout(sessionTimer) - }) - - return { - ready, - handoff: { - tabs: createMemo(() => store.handoff?.tabs), - setTabs(dir: string, id: string) { - setStore("handoff", "tabs", { dir, id, at: Date.now() }) - }, - clearTabs() { - if (!store.handoff?.tabs) return - setStore("handoff", "tabs", undefined) - }, - }, - projects: { - list, - open(directory: string) { - const root = rootFor(directory) - if (server.projects.list().find((x) => x.worktree === root)) return - void globalSync.project.loadSessions(root) - server.projects.open(root) - }, - close(directory: string) { - server.projects.close(directory) - }, - expand(directory: string) { - server.projects.expand(directory) - }, - collapse(directory: string) { - server.projects.collapse(directory) - }, - move(directory: string, toIndex: number) { - server.projects.move(directory, toIndex) - }, - }, - sidebar: { - opened: createMemo(() => store.sidebar.opened), - open() { - setStore("sidebar", "opened", true) - }, - close() { - setStore("sidebar", "opened", false) - }, - toggle() { - setStore("sidebar", "opened", (x) => !x) - }, - width: createMemo(() => store.sidebar.width), - resize(width: number) { - setStore("sidebar", "width", width) - }, - workspaces(directory: string) { - return () => store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false - }, - setWorkspaces(directory: string, value: boolean) { - setStore("sidebar", "workspaces", directory, value) - }, - toggleWorkspaces(directory: string) { - const current = store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false - setStore("sidebar", "workspaces", directory, !current) - }, - }, - terminal: { - height: createMemo(() => store.terminal.height), - resize(height: number) { - setStore("terminal", "height", height) - }, - }, - review: { - diffStyle: createMemo(() => store.review?.diffStyle ?? "split"), - setDiffStyle(diffStyle: ReviewDiffStyle) { - if (!store.review) { - setStore("review", { diffStyle, panelOpened: true }) - return - } - setStore("review", "diffStyle", diffStyle) - }, - }, - fileTree: { - opened: createMemo(() => store.fileTree?.opened ?? true), - width: createMemo(() => store.fileTree?.width ?? DEFAULT_FILE_TREE_WIDTH), - tab: createMemo(() => store.fileTree?.tab ?? "changes"), - setTab(tab: "changes" | "all") { - if (!store.fileTree) { - setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab }) - return - } - setStore("fileTree", "tab", tab) - }, - open() { - if (!store.fileTree) { - setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" }) - return - } - setStore("fileTree", "opened", true) - }, - close() { - if (!store.fileTree) { - setStore("fileTree", { opened: false, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" }) - return - } - setStore("fileTree", "opened", false) - }, - toggle() { - if (!store.fileTree) { - setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" }) - return - } - setStore("fileTree", "opened", (x) => !x) - }, - resize(width: number) { - if (!store.fileTree) { - setStore("fileTree", { opened: true, width, tab: "changes" }) - return - } - setStore("fileTree", "width", width) - }, - }, - session: { - width: createMemo(() => store.session?.width ?? DEFAULT_SESSION_WIDTH), - resize(width: number) { - if (!store.session) { - setStore("session", { width }) - return - } - setStore("session", "width", width) - }, - }, - mobileSidebar: { - opened: createMemo(() => store.mobileSidebar?.opened ?? false), - show() { - setStore("mobileSidebar", "opened", true) - }, - hide() { - setStore("mobileSidebar", "opened", false) - }, - toggle() { - setStore("mobileSidebar", "opened", (x) => !x) - }, - }, - pendingMessage: { - set(sessionKey: string, messageID: string) { - const at = Date.now() - touch(sessionKey) - const current = store.sessionView[sessionKey] - if (!current) { - setStore("sessionView", sessionKey, { - scroll: {}, - pendingMessage: messageID, - pendingMessageAt: at, - }) - prune(usage.active ?? sessionKey) - return - } - - setStore( - "sessionView", - sessionKey, - produce((draft) => { - draft.pendingMessage = messageID - draft.pendingMessageAt = at - }), - ) - }, - consume(sessionKey: string) { - const current = store.sessionView[sessionKey] - const message = current?.pendingMessage - const at = current?.pendingMessageAt - if (!message || !at) return - - setStore( - "sessionView", - sessionKey, - produce((draft) => { - delete draft.pendingMessage - delete draft.pendingMessageAt - }), - ) - - if (Date.now() - at > PENDING_MESSAGE_TTL_MS) return - return message - }, - }, - view(sessionKey: string | Accessor) { - const key = createSessionKeyReader(sessionKey, ensureKey) - const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} }) - const terminalOpened = createMemo(() => store.terminal?.opened ?? false) - const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true) - - function setTerminalOpened(next: boolean) { - const current = store.terminal - if (!current) { - setStore("terminal", { height: DEFAULT_TERMINAL_HEIGHT, opened: next }) - return - } - - const value = current.opened ?? false - if (value === next) return - setStore("terminal", "opened", next) - } - - function setReviewPanelOpened(next: boolean) { - const current = store.review - if (!current) { - setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next }) - return - } - - const value = current.panelOpened ?? true - if (value === next) return - setStore("review", "panelOpened", next) - } - - return { - scroll(tab: string) { - return scroll.scroll(key(), tab) - }, - setScroll(tab: string, pos: SessionScroll) { - scroll.setScroll(key(), tab, pos) - }, - terminal: { - opened: terminalOpened, - open() { - setTerminalOpened(true) - }, - close() { - setTerminalOpened(false) - }, - toggle() { - setTerminalOpened(!terminalOpened()) - }, - }, - reviewPanel: { - opened: reviewPanelOpened, - open() { - setReviewPanelOpened(true) - }, - close() { - setReviewPanelOpened(false) - }, - toggle() { - setReviewPanelOpened(!reviewPanelOpened()) - }, - }, - review: { - open: createMemo(() => s().reviewOpen ?? []), - setOpen(open: string[]) { - const session = key() - const next = Array.from(new Set(open)) - const current = store.sessionView[session] - if (!current) { - setStore("sessionView", session, { - scroll: {}, - reviewOpen: next, - }) - return - } - - if (same(current.reviewOpen, next)) return - setStore("sessionView", session, "reviewOpen", next) - }, - openPath(path: string) { - const session = key() - const current = store.sessionView[session] - if (!current) { - setStore("sessionView", session, { - scroll: {}, - reviewOpen: [path], - }) - return - } - - if (!current.reviewOpen) { - setStore("sessionView", session, "reviewOpen", [path]) - return - } - - if (current.reviewOpen.includes(path)) return - setStore("sessionView", session, "reviewOpen", current.reviewOpen.length, path) - }, - closePath(path: string) { - const session = key() - const current = store.sessionView[session]?.reviewOpen - if (!current) return - - const index = current.indexOf(path) - if (index === -1) return - setStore( - "sessionView", - session, - "reviewOpen", - produce((draft) => { - if (!draft) return - draft.splice(index, 1) - }), - ) - }, - togglePath(path: string) { - const session = key() - const current = store.sessionView[session]?.reviewOpen - if (!current || !current.includes(path)) { - this.openPath(path) - return - } - - this.closePath(path) - }, - }, - } - }, - tabs(sessionKey: string | Accessor) { - const key = createSessionKeyReader(sessionKey, ensureKey) - const path = createMemo(() => sessionPath(key())) - const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] }) - const normalize = (tab: string) => normalizeSessionTab(path(), tab) - const normalizeAll = (all: string[]) => normalizeSessionTabList(path(), all) - return { - tabs, - active: createMemo(() => tabs().active), - all: createMemo(() => tabs().all.filter((tab) => tab !== "review")), - setActive(tab: string | undefined) { - const session = key() - const next = tab ? normalize(tab) : tab - if (!store.sessionTabs[session]) { - setStore("sessionTabs", session, { all: [], active: next }) - } else { - setStore("sessionTabs", session, "active", next) - } - }, - setAll(all: string[]) { - const session = key() - const next = normalizeAll(all).filter((tab) => tab !== "review") - if (!store.sessionTabs[session]) { - setStore("sessionTabs", session, { all: next, active: undefined }) - } else { - setStore("sessionTabs", session, "all", next) - } - }, - async open(tab: string) { - const session = key() - const next = nextSessionTabsForOpen(store.sessionTabs[session], normalize(tab)) - setStore("sessionTabs", session, next) - }, - close(tab: string) { - const session = key() - const current = store.sessionTabs[session] - if (!current) return - - if (tab === "review") { - if (current.active !== tab) return - setStore("sessionTabs", session, "active", current.all[0]) - return - } - - const all = current.all.filter((x) => x !== tab) - if (current.active !== tab) { - setStore("sessionTabs", session, "all", all) - return - } - - const index = current.all.findIndex((f) => f === tab) - const next = current.all[index - 1] ?? current.all[index + 1] ?? all[0] - batch(() => { - setStore("sessionTabs", session, "all", all) - setStore("sessionTabs", session, "active", next) - }) - }, - move(tab: string, to: number) { - const session = key() - const current = store.sessionTabs[session] - if (!current) return - const index = current.all.findIndex((f) => f === tab) - if (index === -1) return - setStore( - "sessionTabs", - session, - "all", - produce((opened) => { - opened.splice(to, 0, opened.splice(index, 1)[0]) - }), - ) - }, - } - }, - } - }, -}) diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx deleted file mode 100644 index 0b0972ee670..00000000000 --- a/packages/app/src/context/local.tsx +++ /dev/null @@ -1,392 +0,0 @@ -import { createSimpleContext } from "@opencode-ai/ui/context" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { useParams } from "@solidjs/router" -import { batch, createEffect, createMemo } from "solid-js" -import { createStore } from "solid-js/store" -import { useModels } from "@/context/models" -import { useProviders } from "@/hooks/use-providers" -import { Persist, persisted } from "@/utils/persist" -import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant" -import { useSDK } from "./sdk" -import { useSync } from "./sync" - -export type ModelKey = { providerID: string; modelID: string; variant?: string } - -type State = { - agent?: string - model?: ModelKey - variant?: string | null -} - -type Saved = { - session: Record -} - -const WORKSPACE_KEY = "__workspace__" -const handoff = new Map() - -const handoffKey = (dir: string, id: string) => `${dir}\n${id}` - -const migrate = (value: unknown) => { - if (!value || typeof value !== "object") return { session: {} } - - const item = value as { - session?: Record - pick?: Record - } - - if (item.session && typeof item.session === "object") return { session: item.session } - if (!item.pick || typeof item.pick !== "object") return { session: {} } - - return { - session: Object.fromEntries(Object.entries(item.pick).filter(([key]) => key !== WORKSPACE_KEY)), - } -} - -const clone = (value: State | undefined) => { - if (!value) return undefined - return { - ...value, - model: value.model ? { ...value.model } : undefined, - } satisfies State -} - -export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ - name: "Local", - init: () => { - const params = useParams() - const sdk = useSDK() - const sync = useSync() - const providers = useProviders() - const models = useModels() - - const id = createMemo(() => params.id || undefined) - const list = createMemo(() => sync.data.agent.filter((item) => item.mode !== "subagent" && !item.hidden)) - const connected = createMemo(() => new Set(providers.connected().map((item) => item.id))) - - const [saved, setSaved] = persisted( - { - ...Persist.workspace(sdk.directory, "model-selection", ["model-selection.v1"]), - migrate, - }, - createStore({ - session: {}, - }), - ) - - const [store, setStore] = createStore<{ - current?: string - draft?: State - last?: { - type: "agent" | "model" | "variant" - agent?: string - model?: ModelKey | null - variant?: string | null - } - }>({ - current: list()[0]?.name, - draft: undefined, - last: undefined, - }) - - const validModel = (model: ModelKey) => { - const provider = providers.all().find((item) => item.id === model.providerID) - return !!provider?.models[model.modelID] && connected().has(model.providerID) - } - - const firstModel = (...items: Array<() => ModelKey | undefined>) => { - for (const item of items) { - const model = item() - if (!model) continue - if (validModel(model)) return model - } - } - - const pickAgent = (name: string | undefined) => { - const items = list() - if (items.length === 0) return undefined - return items.find((item) => item.name === name) ?? items[0] - } - - createEffect(() => { - const items = list() - if (items.length === 0) { - if (store.current !== undefined) setStore("current", undefined) - return - } - if (items.some((item) => item.name === store.current)) return - setStore("current", items[0]?.name) - }) - - const scope = createMemo(() => { - const session = id() - if (!session) return store.draft - return saved.session[session] ?? handoff.get(handoffKey(sdk.directory, session)) - }) - - createEffect(() => { - const session = id() - if (!session) return - - const key = handoffKey(sdk.directory, session) - const next = handoff.get(key) - if (!next) return - if (saved.session[session] !== undefined) { - handoff.delete(key) - return - } - - setSaved("session", session, clone(next)) - handoff.delete(key) - }) - - const configuredModel = () => { - if (!sync.data.config.model) return - const [providerID, modelID] = sync.data.config.model.split("/") - const model = { providerID, modelID } - if (validModel(model)) return model - } - - const recentModel = () => { - for (const item of models.recent.list()) { - if (validModel(item)) return item - } - } - - const defaultModel = () => { - const defaults = providers.default() - for (const provider of providers.connected()) { - const configured = defaults[provider.id] - if (configured) { - const model = { providerID: provider.id, modelID: configured } - if (validModel(model)) return model - } - - const first = Object.values(provider.models)[0] - if (!first) continue - const model = { providerID: provider.id, modelID: first.id } - if (validModel(model)) return model - } - } - - const fallback = createMemo(() => configuredModel() ?? recentModel() ?? defaultModel()) - - const agent = { - list, - current() { - return pickAgent(scope()?.agent ?? store.current) - }, - set(name: string | undefined) { - const item = pickAgent(name) - if (!item) { - setStore("current", undefined) - return - } - - batch(() => { - setStore("current", item.name) - setStore("last", { - type: "agent", - agent: item.name, - model: item.model, - variant: item.variant ?? null, - }) - const prev = scope() - const next = { - agent: item.name, - model: item.model ?? prev?.model, - variant: item.variant ?? prev?.variant, - } satisfies State - const session = id() - if (session) { - setSaved("session", session, next) - return - } - setStore("draft", next) - }) - }, - move(direction: 1 | -1) { - const items = list() - if (items.length === 0) { - setStore("current", undefined) - return - } - - let next = items.findIndex((item) => item.name === agent.current()?.name) + direction - if (next < 0) next = items.length - 1 - if (next >= items.length) next = 0 - const item = items[next] - if (!item) return - agent.set(item.name) - }, - } - - const current = () => { - const item = firstModel( - () => scope()?.model, - () => agent.current()?.model, - fallback, - ) - if (!item) return undefined - return models.find(item) - } - - const configured = () => { - const item = agent.current() - const model = current() - if (!item || !model) return undefined - return getConfiguredAgentVariant({ - agent: { model: item.model, variant: item.variant }, - model: { providerID: model.provider.id, modelID: model.id, variants: model.variants }, - }) - } - - const selected = () => scope()?.variant - - const snapshot = () => { - const model = current() - return { - agent: agent.current()?.name, - model: model ? { providerID: model.provider.id, modelID: model.id } : undefined, - variant: selected(), - } satisfies State - } - - const write = (next: Partial) => { - const state = { - ...(scope() ?? { agent: agent.current()?.name }), - ...next, - } satisfies State - - const session = id() - if (session) { - setSaved("session", session, state) - return - } - setStore("draft", state) - } - - const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean)) - - const model = { - ready: models.ready, - current, - recent, - list: models.list, - cycle(direction: 1 | -1) { - const items = recent() - const item = current() - if (!item) return - - const index = items.findIndex((entry) => entry?.provider.id === item.provider.id && entry?.id === item.id) - if (index === -1) return - - let next = index + direction - if (next < 0) next = items.length - 1 - if (next >= items.length) next = 0 - - const entry = items[next] - if (!entry) return - model.set({ providerID: entry.provider.id, modelID: entry.id }) - }, - set(item: ModelKey | undefined, options?: { recent?: boolean }) { - batch(() => { - setStore("last", { - type: "model", - agent: agent.current()?.name, - model: item ?? null, - variant: selected(), - }) - write({ model: item }) - if (!item) return - models.setVisibility(item, true) - if (!options?.recent) return - models.recent.push(item) - }) - }, - visible(item: ModelKey) { - return models.visible(item) - }, - setVisibility(item: ModelKey, visible: boolean) { - models.setVisibility(item, visible) - }, - variant: { - configured, - selected, - current() { - return resolveModelVariant({ - variants: this.list(), - selected: this.selected(), - configured: this.configured(), - }) - }, - list() { - const item = current() - if (!item?.variants) return [] - return Object.keys(item.variants) - }, - set(value: string | undefined) { - batch(() => { - const model = current() - setStore("last", { - type: "variant", - agent: agent.current()?.name, - model: model ? { providerID: model.provider.id, modelID: model.id } : null, - variant: value ?? null, - }) - write({ variant: value ?? null }) - }) - }, - cycle() { - const items = this.list() - if (items.length === 0) return - this.set( - cycleModelVariant({ - variants: items, - selected: this.selected(), - configured: this.configured(), - }), - ) - }, - }, - } - - const result = { - slug: createMemo(() => base64Encode(sdk.directory)), - model, - agent, - session: { - reset() { - setStore("draft", undefined) - }, - promote(dir: string, session: string) { - const next = clone(snapshot()) - if (!next) return - - if (dir === sdk.directory) { - setSaved("session", session, next) - setStore("draft", undefined) - return - } - - handoff.set(handoffKey(dir, session), next) - setStore("draft", undefined) - }, - restore(msg: { sessionID: string; agent: string; model: ModelKey }) { - const session = id() - if (!session) return - if (msg.sessionID !== session) return - if (saved.session[session] !== undefined) return - if (handoff.has(handoffKey(sdk.directory, session))) return - - setSaved("session", session, { - agent: msg.agent, - model: msg.model, - variant: msg.model.variant ?? null, - }) - }, - }, - } - return result - }, -}) diff --git a/packages/app/src/context/model-variant.test.ts b/packages/app/src/context/model-variant.test.ts deleted file mode 100644 index 583bc5c3dc7..00000000000 --- a/packages/app/src/context/model-variant.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant" - -describe("model variant", () => { - test("resolves configured agent variant when model matches", () => { - const value = getConfiguredAgentVariant({ - agent: { - model: { providerID: "openai", modelID: "gpt-5.2" }, - variant: "xhigh", - }, - model: { - providerID: "openai", - modelID: "gpt-5.2", - variants: { low: {}, high: {}, xhigh: {} }, - }, - }) - - expect(value).toBe("xhigh") - }) - - test("ignores configured variant when model does not match", () => { - const value = getConfiguredAgentVariant({ - agent: { - model: { providerID: "openai", modelID: "gpt-5.2" }, - variant: "xhigh", - }, - model: { - providerID: "anthropic", - modelID: "claude-sonnet-4", - variants: { low: {}, high: {}, xhigh: {} }, - }, - }) - - expect(value).toBeUndefined() - }) - - test("prefers selected variant over configured variant", () => { - const value = resolveModelVariant({ - variants: ["low", "high", "xhigh"], - selected: "high", - configured: "xhigh", - }) - - expect(value).toBe("high") - }) - - test("lets an explicit default override the configured variant", () => { - const value = resolveModelVariant({ - variants: ["low", "high", "xhigh"], - selected: null, - configured: "xhigh", - }) - - expect(value).toBeUndefined() - }) - - test("cycles from configured variant to next", () => { - const value = cycleModelVariant({ - variants: ["low", "high", "xhigh"], - selected: undefined, - configured: "high", - }) - - expect(value).toBe("xhigh") - }) - - test("wraps from configured last variant to first", () => { - const value = cycleModelVariant({ - variants: ["low", "high", "xhigh"], - selected: undefined, - configured: "xhigh", - }) - - expect(value).toBe("low") - }) - - test("cycles from an explicit default to the first variant", () => { - const value = cycleModelVariant({ - variants: ["low", "high", "xhigh"], - selected: null, - configured: "xhigh", - }) - - expect(value).toBe("low") - }) -}) diff --git a/packages/app/src/context/model-variant.ts b/packages/app/src/context/model-variant.ts deleted file mode 100644 index 525acbba321..00000000000 --- a/packages/app/src/context/model-variant.ts +++ /dev/null @@ -1,52 +0,0 @@ -type AgentModel = { - providerID: string - modelID: string -} - -type Agent = { - model?: AgentModel - variant?: string -} - -type Model = AgentModel & { - variants?: Record -} - -type VariantInput = { - variants: string[] - selected: string | null | undefined - configured: string | undefined -} - -export function getConfiguredAgentVariant(input: { agent: Agent | undefined; model: Model | undefined }) { - if (!input.agent?.variant) return undefined - if (!input.agent.model) return undefined - if (!input.model?.variants) return undefined - if (input.agent.model.providerID !== input.model.providerID) return undefined - if (input.agent.model.modelID !== input.model.modelID) return undefined - if (!(input.agent.variant in input.model.variants)) return undefined - return input.agent.variant -} - -export function resolveModelVariant(input: VariantInput) { - if (input.selected === null) return undefined - if (input.selected && input.variants.includes(input.selected)) return input.selected - if (input.configured && input.variants.includes(input.configured)) return input.configured - return undefined -} - -export function cycleModelVariant(input: VariantInput) { - if (input.variants.length === 0) return undefined - if (input.selected === null) return input.variants[0] - if (input.selected && input.variants.includes(input.selected)) { - const index = input.variants.indexOf(input.selected) - if (index === input.variants.length - 1) return undefined - return input.variants[index + 1] - } - if (input.configured && input.variants.includes(input.configured)) { - const index = input.variants.indexOf(input.configured) - if (index === input.variants.length - 1) return input.variants[0] - return input.variants[index + 1] - } - return input.variants[0] -} diff --git a/packages/app/src/context/models.tsx b/packages/app/src/context/models.tsx deleted file mode 100644 index 12ec8371add..00000000000 --- a/packages/app/src/context/models.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { createMemo } from "solid-js" -import { createStore } from "solid-js/store" -import { DateTime } from "luxon" -import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda" -import { createSimpleContext } from "@opencode-ai/ui/context" -import { useProviders } from "@/hooks/use-providers" -import { Persist, persisted } from "@/utils/persist" - -export type ModelKey = { providerID: string; modelID: string } - -type Visibility = "show" | "hide" -type User = ModelKey & { visibility: Visibility; favorite?: boolean } -type Store = { - user: User[] - recent: ModelKey[] - variant?: Record -} - -const RECENT_LIMIT = 5 - -function modelKey(model: ModelKey) { - return `${model.providerID}:${model.modelID}` -} - -export const { use: useModels, provider: ModelsProvider } = createSimpleContext({ - name: "Models", - init: () => { - const providers = useProviders() - - const [store, setStore, _, ready] = persisted( - Persist.global("model", ["model.v1"]), - createStore({ - user: [], - recent: [], - variant: {}, - }), - ) - - const available = createMemo(() => - providers.connected().flatMap((p) => - Object.values(p.models).map((m) => ({ - ...m, - provider: p, - })), - ), - ) - - const release = createMemo( - () => - new Map( - available().map((model) => { - const parsed = DateTime.fromISO(model.release_date) - return [modelKey({ providerID: model.provider.id, modelID: model.id }), parsed] as const - }), - ), - ) - - const latest = createMemo(() => - pipe( - available(), - filter( - (x) => - Math.abs( - (release().get(modelKey({ providerID: x.provider.id, modelID: x.id })) ?? DateTime.invalid("invalid")) - .diffNow() - .as("months"), - ) < 6, - ), - groupBy((x) => x.provider.id), - mapValues((models) => - pipe( - models, - groupBy((x) => x.family), - values(), - (groups) => - groups.flatMap((g) => { - const first = firstBy(g, [(x) => x.release_date, "desc"]) - return first ? [{ modelID: first.id, providerID: first.provider.id }] : [] - }), - ), - ), - values(), - flat(), - ), - ) - - const latestSet = createMemo(() => new Set(latest().map((x) => modelKey(x)))) - - const visibility = createMemo(() => { - const map = new Map() - for (const item of store.user) map.set(`${item.providerID}:${item.modelID}`, item.visibility) - return map - }) - - const list = createMemo(() => - available().map((m) => ({ - ...m, - name: m.name.replace("(latest)", "").trim(), - latest: m.name.includes("(latest)"), - })), - ) - - const find = (key: ModelKey) => list().find((m) => m.id === key.modelID && m.provider.id === key.providerID) - - function update(model: ModelKey, state: Visibility) { - const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID) - if (index >= 0) { - setStore("user", index, (current) => ({ ...current, visibility: state })) - return - } - setStore("user", store.user.length, { ...model, visibility: state }) - } - - const visible = (model: ModelKey) => { - const key = modelKey(model) - const state = visibility().get(key) - if (state === "hide") return false - if (state === "show") return true - if (latestSet().has(key)) return true - const date = release().get(key) - if (!date?.isValid) return true - return false - } - - const setVisibility = (model: ModelKey, state: boolean) => { - update(model, state ? "show" : "hide") - } - - const push = (model: ModelKey) => { - const uniq = uniqueBy([model, ...store.recent], (x) => `${x.providerID}:${x.modelID}`) - if (uniq.length > RECENT_LIMIT) uniq.pop() - setStore("recent", uniq) - } - - const variantKey = (model: ModelKey) => `${model.providerID}/${model.modelID}` - const getVariant = (model: ModelKey) => store.variant?.[variantKey(model)] - - const setVariant = (model: ModelKey, value: string | undefined) => { - const key = variantKey(model) - if (!store.variant) { - setStore("variant", { [key]: value }) - return - } - setStore("variant", key, value) - } - - return { - ready, - list, - find, - visible, - setVisibility, - recent: { - list: createMemo(() => store.recent), - push, - }, - variant: { - get: getVariant, - set: setVariant, - }, - } - }, -}) diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx deleted file mode 100644 index 9c30ed269d6..00000000000 --- a/packages/app/src/context/notification.tsx +++ /dev/null @@ -1,373 +0,0 @@ -import { createStore, reconcile } from "solid-js/store" -import { batch, createEffect, createMemo, onCleanup } from "solid-js" -import { useParams } from "@solidjs/router" -import { createSimpleContext } from "@opencode-ai/ui/context" -import { useGlobalSDK } from "./global-sdk" -import { useGlobalSync } from "./global-sync" -import { usePlatform } from "@/context/platform" -import { useLanguage } from "@/context/language" -import { useSettings } from "@/context/settings" -import { Binary } from "@opencode-ai/shared/util/binary" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { decode64 } from "@/utils/base64" -import { EventSessionError } from "@kilocode/sdk/v2" -import { Persist, persisted } from "@/utils/persist" -import { playSoundById } from "@/utils/sound" - -type NotificationBase = { - directory?: string - session?: string - metadata?: unknown - time: number - viewed: boolean -} - -type TurnCompleteNotification = NotificationBase & { - type: "turn-complete" -} - -type ErrorNotification = NotificationBase & { - type: "error" - error: EventSessionError["properties"]["error"] -} - -export type Notification = TurnCompleteNotification | ErrorNotification - -type NotificationIndex = { - session: { - all: Record - unseen: Record - unseenCount: Record - unseenHasError: Record - } - project: { - all: Record - unseen: Record - unseenCount: Record - unseenHasError: Record - } -} - -const MAX_NOTIFICATIONS = 500 -const NOTIFICATION_TTL_MS = 1000 * 60 * 60 * 24 * 30 - -function pruneNotifications(list: Notification[]) { - const cutoff = Date.now() - NOTIFICATION_TTL_MS - const pruned = list.filter((n) => n.time >= cutoff) - if (pruned.length <= MAX_NOTIFICATIONS) return pruned - return pruned.slice(pruned.length - MAX_NOTIFICATIONS) -} - -function createNotificationIndex(): NotificationIndex { - return { - session: { - all: {}, - unseen: {}, - unseenCount: {}, - unseenHasError: {}, - }, - project: { - all: {}, - unseen: {}, - unseenCount: {}, - unseenHasError: {}, - }, - } -} - -function buildNotificationIndex(list: Notification[]) { - const index = createNotificationIndex() - - list.forEach((notification) => { - if (notification.session) { - const all = index.session.all[notification.session] ?? [] - index.session.all[notification.session] = [...all, notification] - if (!notification.viewed) { - const unseen = index.session.unseen[notification.session] ?? [] - index.session.unseen[notification.session] = [...unseen, notification] - index.session.unseenCount[notification.session] = unseen.length + 1 - if (notification.type === "error") index.session.unseenHasError[notification.session] = true - } - } - - if (notification.directory) { - const all = index.project.all[notification.directory] ?? [] - index.project.all[notification.directory] = [...all, notification] - if (!notification.viewed) { - const unseen = index.project.unseen[notification.directory] ?? [] - index.project.unseen[notification.directory] = [...unseen, notification] - index.project.unseenCount[notification.directory] = unseen.length + 1 - if (notification.type === "error") index.project.unseenHasError[notification.directory] = true - } - } - }) - - return index -} - -export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({ - name: "Notification", - init: () => { - const params = useParams() - const globalSDK = useGlobalSDK() - const globalSync = useGlobalSync() - const platform = usePlatform() - const settings = useSettings() - const language = useLanguage() - - const empty: Notification[] = [] - - const currentDirectory = createMemo(() => { - return decode64(params.dir) - }) - - const currentSession = createMemo(() => params.id) - - const [store, setStore, _, ready] = persisted( - Persist.global("notification", ["notification.v1"]), - createStore({ - list: [] as Notification[], - }), - ) - const [index, setIndex] = createStore(buildNotificationIndex(store.list)) - - const meta = { pruned: false, disposed: false } - - const updateUnseen = (scope: "session" | "project", key: string, unseen: Notification[]) => { - setIndex(scope, "unseen", key, unseen) - setIndex(scope, "unseenCount", key, unseen.length) - setIndex( - scope, - "unseenHasError", - key, - unseen.some((notification) => notification.type === "error"), - ) - } - - const appendToIndex = (notification: Notification) => { - if (notification.session) { - setIndex("session", "all", notification.session, (all = []) => [...all, notification]) - if (!notification.viewed) { - setIndex("session", "unseen", notification.session, (unseen = []) => [...unseen, notification]) - setIndex("session", "unseenCount", notification.session, (count = 0) => count + 1) - if (notification.type === "error") setIndex("session", "unseenHasError", notification.session, true) - } - } - - if (notification.directory) { - setIndex("project", "all", notification.directory, (all = []) => [...all, notification]) - if (!notification.viewed) { - setIndex("project", "unseen", notification.directory, (unseen = []) => [...unseen, notification]) - setIndex("project", "unseenCount", notification.directory, (count = 0) => count + 1) - if (notification.type === "error") setIndex("project", "unseenHasError", notification.directory, true) - } - } - } - - const removeFromIndex = (notification: Notification) => { - if (notification.session) { - setIndex("session", "all", notification.session, (all = []) => all.filter((n) => n !== notification)) - if (!notification.viewed) { - const unseen = (index.session.unseen[notification.session] ?? empty).filter((n) => n !== notification) - updateUnseen("session", notification.session, unseen) - } - } - - if (notification.directory) { - setIndex("project", "all", notification.directory, (all = []) => all.filter((n) => n !== notification)) - if (!notification.viewed) { - const unseen = (index.project.unseen[notification.directory] ?? empty).filter((n) => n !== notification) - updateUnseen("project", notification.directory, unseen) - } - } - } - - createEffect(() => { - if (!ready()) return - if (meta.pruned) return - meta.pruned = true - const list = pruneNotifications(store.list) - batch(() => { - setStore("list", list) - setIndex(reconcile(buildNotificationIndex(list), { merge: false })) - }) - }) - - const append = (notification: Notification) => { - const list = pruneNotifications([...store.list, notification]) - const keep = new Set(list) - const removed = store.list.filter((n) => !keep.has(n)) - - batch(() => { - if (keep.has(notification)) appendToIndex(notification) - removed.forEach((n) => removeFromIndex(n)) - setStore("list", list) - }) - } - - const lookup = async (directory: string, sessionID?: string) => { - if (!sessionID) return undefined - const [syncStore] = globalSync.child(directory, { bootstrap: false }) - const match = Binary.search(syncStore.session, sessionID, (s) => s.id) - if (match.found) return syncStore.session[match.index] - return globalSDK.client.session - .get({ directory, sessionID }) - .then((x) => x.data) - .catch(() => undefined) - } - - const viewedInCurrentSession = (directory: string, sessionID?: string) => { - const activeDirectory = currentDirectory() - const activeSession = currentSession() - if (!activeDirectory) return false - if (!activeSession) return false - if (!sessionID) return false - if (directory !== activeDirectory) return false - return sessionID === activeSession - } - - const handleSessionIdle = (directory: string, event: { properties: { sessionID?: string } }, time: number) => { - const sessionID = event.properties.sessionID - void lookup(directory, sessionID).then((session) => { - if (meta.disposed) return - if (!session) return - if (session.parentID) return - - if (settings.sounds.agentEnabled()) { - void playSoundById(settings.sounds.agent()) - } - - append({ - directory, - time, - viewed: viewedInCurrentSession(directory, sessionID), - type: "turn-complete", - session: sessionID, - }) - - const href = `/${base64Encode(directory)}/session/${sessionID}` - if (settings.notifications.agent()) { - void platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, href) - } - }) - } - - const handleSessionError = ( - directory: string, - event: { properties: { sessionID?: string; error?: EventSessionError["properties"]["error"] } }, - time: number, - ) => { - const sessionID = event.properties.sessionID - void lookup(directory, sessionID).then((session) => { - if (meta.disposed) return - if (session?.parentID) return - - if (settings.sounds.errorsEnabled()) { - void playSoundById(settings.sounds.errors()) - } - - const error = "error" in event.properties ? event.properties.error : undefined - append({ - directory, - time, - viewed: viewedInCurrentSession(directory, sessionID), - type: "error", - session: sessionID ?? "global", - error, - }) - const description = - session?.title ?? - (typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription")) - const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}` - if (settings.notifications.errors()) { - void platform.notify(language.t("notification.session.error.title"), description, href) - } - }) - } - - const unsub = globalSDK.event.listen((e) => { - const event = e.details - if (event.type !== "session.idle" && event.type !== "session.error") return - - const directory = e.name - const time = Date.now() - if (event.type === "session.idle") { - handleSessionIdle(directory, event, time) - return - } - handleSessionError(directory, event, time) - }) - onCleanup(() => { - meta.disposed = true - unsub() - }) - - return { - ready, - session: { - all(session: string) { - return index.session.all[session] ?? empty - }, - unseen(session: string) { - return index.session.unseen[session] ?? empty - }, - unseenCount(session: string) { - return index.session.unseenCount[session] ?? 0 - }, - unseenHasError(session: string) { - return index.session.unseenHasError[session] ?? false - }, - markViewed(session: string) { - const unseen = index.session.unseen[session] ?? empty - if (!unseen.length) return - - const projects = [ - ...new Set(unseen.flatMap((notification) => (notification.directory ? [notification.directory] : []))), - ] - batch(() => { - setStore("list", (n) => n.session === session && !n.viewed, "viewed", true) - updateUnseen("session", session, []) - projects.forEach((directory) => { - const next = (index.project.unseen[directory] ?? empty).filter( - (notification) => notification.session !== session, - ) - updateUnseen("project", directory, next) - }) - }) - }, - }, - project: { - all(directory: string) { - return index.project.all[directory] ?? empty - }, - unseen(directory: string) { - return index.project.unseen[directory] ?? empty - }, - unseenCount(directory: string) { - return index.project.unseenCount[directory] ?? 0 - }, - unseenHasError(directory: string) { - return index.project.unseenHasError[directory] ?? false - }, - markViewed(directory: string) { - const unseen = index.project.unseen[directory] ?? empty - if (!unseen.length) return - - const sessions = [ - ...new Set(unseen.flatMap((notification) => (notification.session ? [notification.session] : []))), - ] - batch(() => { - setStore("list", (n) => n.directory === directory && !n.viewed, "viewed", true) - updateUnseen("project", directory, []) - sessions.forEach((session) => { - const next = (index.session.unseen[session] ?? empty).filter( - (notification) => notification.directory !== directory, - ) - updateUnseen("session", session, next) - }) - }) - }, - }, - } - }, -}) diff --git a/packages/app/src/context/permission-auto-respond.test.ts b/packages/app/src/context/permission-auto-respond.test.ts deleted file mode 100644 index cb927bbd317..00000000000 --- a/packages/app/src/context/permission-auto-respond.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { PermissionRequest, Session } from "@kilocode/sdk/v2/client" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond" - -const session = (input: { id: string; parentID?: string }) => - ({ - id: input.id, - parentID: input.parentID, - }) as Session - -const permission = (sessionID: string) => - ({ - sessionID, - }) as Pick - -describe("autoRespondsPermission", () => { - test("uses a parent session's directory-scoped auto-accept", () => { - const directory = "/tmp/project" - const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] - const autoAccept = { - [`${base64Encode(directory)}/root`]: true, - } - - expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true) - }) - - test("uses a parent session's legacy auto-accept key", () => { - const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] - - expect(autoRespondsPermission({ root: true }, sessions, permission("child"), "/tmp/project")).toBe(true) - }) - - test("defaults to requiring approval when no lineage override exists", () => { - const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" }), session({ id: "other" })] - const autoAccept = { - other: true, - } - - expect(autoRespondsPermission(autoAccept, sessions, permission("child"), "/tmp/project")).toBe(false) - }) - - test("inherits a parent session's false override", () => { - const directory = "/tmp/project" - const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] - const autoAccept = { - [`${base64Encode(directory)}/root`]: false, - } - - expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(false) - }) - - test("prefers a child override over parent override", () => { - const directory = "/tmp/project" - const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] - const autoAccept = { - [`${base64Encode(directory)}/root`]: false, - [`${base64Encode(directory)}/child`]: true, - } - - expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true) - }) - - test("falls back to directory-level auto-accept", () => { - const directory = "/tmp/project" - const sessions = [session({ id: "root" })] - const autoAccept = { - [`${base64Encode(directory)}/*`]: true, - } - - expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(true) - }) - - test("session-level override takes precedence over directory-level", () => { - const directory = "/tmp/project" - const sessions = [session({ id: "root" })] - const autoAccept = { - [`${base64Encode(directory)}/*`]: true, - [`${base64Encode(directory)}/root`]: false, - } - - expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(false) - }) -}) - -describe("isDirectoryAutoAccepting", () => { - test("returns true when directory key is set", () => { - const directory = "/tmp/project" - const autoAccept = { [`${base64Encode(directory)}/*`]: true } - expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(true) - }) - - test("returns false when directory key is not set", () => { - expect(isDirectoryAutoAccepting({}, "/tmp/project")).toBe(false) - }) - - test("returns false when directory key is explicitly false", () => { - const directory = "/tmp/project" - const autoAccept = { [`${base64Encode(directory)}/*`]: false } - expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(false) - }) -}) diff --git a/packages/app/src/context/permission-auto-respond.ts b/packages/app/src/context/permission-auto-respond.ts deleted file mode 100644 index 2ebca34347d..00000000000 --- a/packages/app/src/context/permission-auto-respond.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { base64Encode } from "@opencode-ai/shared/util/encode" - -export function acceptKey(sessionID: string, directory?: string) { - if (!directory) return sessionID - return `${base64Encode(directory)}/${sessionID}` -} - -export function directoryAcceptKey(directory: string) { - return `${base64Encode(directory)}/*` -} - -function accepted(autoAccept: Record, sessionID: string, directory?: string) { - const key = acceptKey(sessionID, directory) - const directoryKey = directory ? directoryAcceptKey(directory) : undefined - return autoAccept[key] ?? autoAccept[sessionID] ?? (directoryKey ? autoAccept[directoryKey] : undefined) -} - -export function isDirectoryAutoAccepting(autoAccept: Record, directory: string) { - const key = directoryAcceptKey(directory) - return autoAccept[key] ?? false -} - -function sessionLineage(session: { id: string; parentID?: string }[], sessionID: string) { - const parent = session.reduce((acc, item) => { - if (item.parentID) acc.set(item.id, item.parentID) - return acc - }, new Map()) - const seen = new Set([sessionID]) - const ids = [sessionID] - - for (const id of ids) { - const parentID = parent.get(id) - if (!parentID || seen.has(parentID)) continue - seen.add(parentID) - ids.push(parentID) - } - - return ids -} - -export function autoRespondsPermission( - autoAccept: Record, - session: { id: string; parentID?: string }[], - permission: { sessionID: string }, - directory?: string, -) { - const value = sessionLineage(session, permission.sessionID) - .map((id) => accepted(autoAccept, id, directory)) - .find((item): item is boolean => item !== undefined) - return value ?? false -} diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx deleted file mode 100644 index 6f890da7c96..00000000000 --- a/packages/app/src/context/permission.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import { createEffect, createMemo, onCleanup } from "solid-js" -import { createStore, produce } from "solid-js/store" -import { createSimpleContext } from "@opencode-ai/ui/context" -import type { PermissionRequest } from "@kilocode/sdk/v2/client" -import { Persist, persisted } from "@/utils/persist" -import { useGlobalSDK } from "@/context/global-sdk" -import { useGlobalSync } from "./global-sync" -import { useParams } from "@solidjs/router" -import { decode64 } from "@/utils/base64" -import { - acceptKey, - directoryAcceptKey, - isDirectoryAutoAccepting, - autoRespondsPermission, -} from "./permission-auto-respond" - -type PermissionRespondFn = (input: { - sessionID: string - permissionID: string - response: "once" | "always" | "reject" - directory?: string -}) => void - -function isNonAllowRule(rule: unknown) { - if (!rule) return false - if (typeof rule === "string") return rule !== "allow" - if (typeof rule !== "object") return false - if (Array.isArray(rule)) return false - - for (const action of Object.values(rule)) { - if (action !== "allow") return true - } - - return false -} - -function hasPermissionPromptRules(permission: unknown) { - if (!permission) return false - if (typeof permission === "string") return permission !== "allow" - if (typeof permission !== "object") return false - if (Array.isArray(permission)) return false - - const config = permission as Record - return Object.values(config).some(isNonAllowRule) -} - -export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({ - name: "Permission", - init: () => { - const params = useParams() - const globalSDK = useGlobalSDK() - const globalSync = useGlobalSync() - - const permissionsEnabled = createMemo(() => { - const directory = decode64(params.dir) - if (!directory) return false - const [store] = globalSync.child(directory) - return hasPermissionPromptRules(store.config.permission) - }) - - const [store, setStore, _, ready] = persisted( - { - ...Persist.global("permission", ["permission.v3"]), - migrate(value) { - if (!value || typeof value !== "object" || Array.isArray(value)) return value - - const data = value as Record - if (data.autoAccept) return value - - return { - ...data, - autoAccept: - typeof data.autoAcceptEdits === "object" && data.autoAcceptEdits && !Array.isArray(data.autoAcceptEdits) - ? data.autoAcceptEdits - : {}, - } - }, - }, - createStore({ - autoAccept: {} as Record, - }), - ) - - // When config has permission: "allow", auto-enable directory-level auto-accept - createEffect(() => { - if (!ready()) return - const directory = decode64(params.dir) - if (!directory) return - const [childStore] = globalSync.child(directory) - const perm = childStore.config.permission - if (typeof perm === "string" && perm === "allow") { - const key = directoryAcceptKey(directory) - if (store.autoAccept[key] === undefined) { - setStore( - produce((draft) => { - draft.autoAccept[key] = true - }), - ) - } - } - }) - - const MAX_RESPONDED = 1000 - const RESPONDED_TTL_MS = 60 * 60 * 1000 - const responded = new Map() - const enableVersion = new Map() - - function pruneResponded(now: number) { - for (const [id, ts] of responded) { - if (now - ts < RESPONDED_TTL_MS) break - responded.delete(id) - } - - for (const id of responded.keys()) { - if (responded.size <= MAX_RESPONDED) break - responded.delete(id) - } - } - - const respond: PermissionRespondFn = (input) => { - globalSDK.client.permission.respond(input).catch(() => { - responded.delete(input.permissionID) - }) - } - - function respondOnce(permission: PermissionRequest, directory?: string) { - const now = Date.now() - const hit = responded.has(permission.id) - responded.delete(permission.id) - responded.set(permission.id, now) - pruneResponded(now) - if (hit) return - respond({ - sessionID: permission.sessionID, - permissionID: permission.id, - response: "once", - directory, - }) - } - - function isAutoAccepting(sessionID: string, directory?: string) { - const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : [] - return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory) - } - - function isAutoAcceptingDirectory(directory: string) { - return isDirectoryAutoAccepting(store.autoAccept, directory) - } - - function shouldAutoRespond(permission: PermissionRequest, directory?: string) { - const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : [] - return autoRespondsPermission(store.autoAccept, session, permission, directory) - } - - function bumpEnableVersion(sessionID: string, directory?: string) { - const key = acceptKey(sessionID, directory) - const next = (enableVersion.get(key) ?? 0) + 1 - enableVersion.set(key, next) - return next - } - - const unsubscribe = globalSDK.event.listen((e) => { - const event = e.details - if (event?.type !== "permission.asked") return - - const perm = event.properties - if (!shouldAutoRespond(perm, e.name)) return - - respondOnce(perm, e.name) - }) - onCleanup(unsubscribe) - - function enableDirectory(directory: string) { - const key = directoryAcceptKey(directory) - setStore( - produce((draft) => { - draft.autoAccept[key] = true - }), - ) - - globalSDK.client.permission - .list({ directory }) - .then((x) => { - if (!isAutoAcceptingDirectory(directory)) return - for (const perm of x.data ?? []) { - if (!perm?.id) continue - if (!shouldAutoRespond(perm, directory)) continue - respondOnce(perm, directory) - } - }) - .catch(() => undefined) - } - - function disableDirectory(directory: string) { - const key = directoryAcceptKey(directory) - setStore( - produce((draft) => { - draft.autoAccept[key] = false - }), - ) - } - - function enable(sessionID: string, directory: string) { - const key = acceptKey(sessionID, directory) - const version = bumpEnableVersion(sessionID, directory) - setStore( - produce((draft) => { - draft.autoAccept[key] = true - delete draft.autoAccept[sessionID] - }), - ) - - globalSDK.client.permission - .list({ directory }) - .then((x) => { - if (enableVersion.get(key) !== version) return - if (!isAutoAccepting(sessionID, directory)) return - for (const perm of x.data ?? []) { - if (!perm?.id) continue - if (!shouldAutoRespond(perm, directory)) continue - respondOnce(perm, directory) - } - }) - .catch(() => undefined) - } - - function disable(sessionID: string, directory?: string) { - bumpEnableVersion(sessionID, directory) - const key = directory ? acceptKey(sessionID, directory) : sessionID - setStore( - produce((draft) => { - draft.autoAccept[key] = false - if (!directory) return - delete draft.autoAccept[sessionID] - }), - ) - } - - return { - ready, - respond, - autoResponds(permission: PermissionRequest, directory?: string) { - return shouldAutoRespond(permission, directory) - }, - isAutoAccepting, - isAutoAcceptingDirectory, - toggleAutoAccept(sessionID: string, directory: string) { - if (isAutoAccepting(sessionID, directory)) { - disable(sessionID, directory) - return - } - - enable(sessionID, directory) - }, - toggleAutoAcceptDirectory(directory: string) { - if (isAutoAcceptingDirectory(directory)) { - disableDirectory(directory) - return - } - enableDirectory(directory) - }, - enableAutoAccept(sessionID: string, directory: string) { - if (isAutoAccepting(sessionID, directory)) return - enable(sessionID, directory) - }, - disableAutoAccept(sessionID: string, directory?: string) { - disable(sessionID, directory) - }, - permissionsEnabled, - isPermissionAllowAll(directory: string) { - const [childStore] = globalSync.child(directory) - const perm = childStore.config.permission - return typeof perm === "string" && perm === "allow" - }, - } - }, -}) diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx deleted file mode 100644 index fd89bf51ba7..00000000000 --- a/packages/app/src/context/platform.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { createSimpleContext } from "@opencode-ai/ui/context" -import type { AsyncStorage, SyncStorage } from "@solid-primitives/storage" -import type { Accessor } from "solid-js" -import { ServerConnection } from "./server" - -type PickerPaths = string | string[] | null -type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean } -type OpenFilePickerOptions = { title?: string; multiple?: boolean; accept?: string[]; extensions?: string[] } -type SaveFilePickerOptions = { title?: string; defaultPath?: string } -type UpdateInfo = { updateAvailable: boolean; version?: string } - -export type Platform = { - /** Platform discriminator */ - platform: "web" | "desktop" - - /** Desktop OS (Tauri only) */ - os?: "macos" | "windows" | "linux" - - /** App version */ - version?: string - - /** Open a URL in the default browser */ - openLink(url: string): void - - /** Open a local path in a local app (desktop only) */ - openPath?(path: string, app?: string): Promise - - /** Restart the app */ - restart(): Promise - - /** Navigate back in history */ - back(): void - - /** Navigate forward in history */ - forward(): void - - /** Send a system notification (optional deep link) */ - notify(title: string, description?: string, href?: string): Promise - - /** Open directory picker dialog (native on Tauri, server-backed on web) */ - openDirectoryPickerDialog?(opts?: OpenDirectoryPickerOptions): Promise - - /** Open native file picker dialog (Tauri only) */ - openFilePickerDialog?(opts?: OpenFilePickerOptions): Promise - - /** Save file picker dialog (Tauri only) */ - saveFilePickerDialog?(opts?: SaveFilePickerOptions): Promise - - /** Storage mechanism, defaults to localStorage */ - storage?: (name?: string) => SyncStorage | AsyncStorage - - /** Check for a downloadable desktop update */ - checkUpdate?(): Promise - - /** Install the downloaded update using the platform restart flow */ - updateAndRestart?(): Promise - - /** Fetch override */ - fetch?: typeof fetch - - /** Get the configured default server URL (platform-specific) */ - getDefaultServer?(): Promise - - /** Set the default server URL to use on app startup (platform-specific) */ - setDefaultServer?(url: ServerConnection.Key | null): Promise | void - - /** Get the configured WSL integration (desktop only) */ - getWslEnabled?(): Promise - - /** Set the configured WSL integration (desktop only) */ - setWslEnabled?(config: boolean): Promise | void - - /** Get the preferred display backend (desktop only) */ - getDisplayBackend?(): Promise | DisplayBackend | null - - /** Set the preferred display backend (desktop only) */ - setDisplayBackend?(backend: DisplayBackend): Promise - - /** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */ - parseMarkdown?(markdown: string): Promise - - /** Webview zoom level (desktop only) */ - webviewZoom?: Accessor - - /** Check if an editor app exists (desktop only) */ - checkAppExists?(appName: string): Promise - - /** Read image from clipboard (desktop only) */ - readClipboardImage?(): Promise -} - -export type DisplayBackend = "auto" | "wayland" - -export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({ - name: "Platform", - init: (props: { value: Platform }) => { - return props.value - }, -}) diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx deleted file mode 100644 index 15af57b355e..00000000000 --- a/packages/app/src/context/prompt.tsx +++ /dev/null @@ -1,297 +0,0 @@ -import { createSimpleContext } from "@opencode-ai/ui/context" -import { checksum } from "@opencode-ai/shared/util/encode" -import { useParams } from "@solidjs/router" -import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js" -import { createStore, type SetStoreFunction } from "solid-js/store" -import type { FileSelection } from "@/context/file" -import { Persist, persisted } from "@/utils/persist" - -interface PartBase { - content: string - start: number - end: number -} - -export interface TextPart extends PartBase { - type: "text" -} - -export interface FileAttachmentPart extends PartBase { - type: "file" - path: string - selection?: FileSelection -} - -export interface AgentPart extends PartBase { - type: "agent" - name: string -} - -export interface ImageAttachmentPart { - type: "image" - id: string - filename: string - mime: string - dataUrl: string -} - -export type ContentPart = TextPart | FileAttachmentPart | AgentPart | ImageAttachmentPart -export type Prompt = ContentPart[] - -export type FileContextItem = { - type: "file" - path: string - selection?: FileSelection - comment?: string - commentID?: string - commentOrigin?: "review" | "file" - preview?: string -} - -export type ContextItem = FileContextItem - -export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }] - -function isSelectionEqual(a?: FileSelection, b?: FileSelection) { - if (!a && !b) return true - if (!a || !b) return false - return ( - a.startLine === b.startLine && a.startChar === b.startChar && a.endLine === b.endLine && a.endChar === b.endChar - ) -} - -function isPartEqual(partA: ContentPart, partB: ContentPart) { - switch (partA.type) { - case "text": - return partB.type === "text" && partA.content === partB.content - case "file": - return partB.type === "file" && partA.path === partB.path && isSelectionEqual(partA.selection, partB.selection) - case "agent": - return partB.type === "agent" && partA.name === partB.name - case "image": - return partB.type === "image" && partA.id === partB.id - } -} - -export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean { - if (promptA.length !== promptB.length) return false - for (let i = 0; i < promptA.length; i++) { - if (!isPartEqual(promptA[i], promptB[i])) return false - } - return true -} - -function cloneSelection(selection?: FileSelection) { - if (!selection) return undefined - return { ...selection } -} - -function clonePart(part: ContentPart): ContentPart { - if (part.type === "text") return { ...part } - if (part.type === "image") return { ...part } - if (part.type === "agent") return { ...part } - return { - ...part, - selection: cloneSelection(part.selection), - } -} - -function clonePrompt(prompt: Prompt): Prompt { - return prompt.map(clonePart) -} - -function contextItemKey(item: ContextItem) { - if (item.type !== "file") return item.type - const start = item.selection?.startLine - const end = item.selection?.endLine - const key = `${item.type}:${item.path}:${start}:${end}` - - if (item.commentID) { - return `${key}:c=${item.commentID}` - } - - const comment = item.comment?.trim() - if (!comment) return key - const digest = checksum(comment) ?? comment - return `${key}:c=${digest.slice(0, 8)}` -} - -function isCommentItem(item: ContextItem | (ContextItem & { key: string })) { - return item.type === "file" && !!item.comment?.trim() -} - -function createPromptActions( - setStore: SetStoreFunction<{ - prompt: Prompt - cursor?: number - context: { - items: (ContextItem & { key: string })[] - } - }>, -) { - return { - set(prompt: Prompt, cursorPosition?: number) { - const next = clonePrompt(prompt) - batch(() => { - setStore("prompt", next) - if (cursorPosition !== undefined) setStore("cursor", cursorPosition) - }) - }, - reset() { - batch(() => { - setStore("prompt", clonePrompt(DEFAULT_PROMPT)) - setStore("cursor", 0) - }) - }, - } -} - -const WORKSPACE_KEY = "__workspace__" -const MAX_PROMPT_SESSIONS = 20 - -type PromptSession = ReturnType - -type Scope = { - dir: string - id?: string -} - -type PromptCacheEntry = { - value: PromptSession - dispose: VoidFunction -} - -function createPromptSession(dir: string, id: string | undefined) { - const legacy = `${dir}/prompt${id ? "/" + id : ""}.v2` - - const [store, setStore, _, ready] = persisted( - Persist.scoped(dir, id, "prompt", [legacy]), - createStore<{ - prompt: Prompt - cursor?: number - context: { - items: (ContextItem & { key: string })[] - } - }>({ - prompt: clonePrompt(DEFAULT_PROMPT), - cursor: undefined, - context: { - items: [], - }, - }), - ) - - const actions = createPromptActions(setStore) - - return { - ready, - current: () => store.prompt, - cursor: createMemo(() => store.cursor), - dirty: () => !isPromptEqual(store.prompt, DEFAULT_PROMPT), - context: { - items: createMemo(() => store.context.items), - add(item: ContextItem) { - const key = contextItemKey(item) - if (store.context.items.find((x) => x.key === key)) return - setStore("context", "items", (items) => [...items, { key, ...item }]) - }, - remove(key: string) { - setStore("context", "items", (items) => items.filter((x) => x.key !== key)) - }, - removeComment(path: string, commentID: string) { - setStore("context", "items", (items) => - items.filter((item) => !(item.type === "file" && item.path === path && item.commentID === commentID)), - ) - }, - updateComment(path: string, commentID: string, next: Partial & { comment?: string }) { - setStore("context", "items", (items) => - items.map((item) => { - if (item.type !== "file" || item.path !== path || item.commentID !== commentID) return item - const value = { ...item, ...next } - return { ...value, key: contextItemKey(value) } - }), - ) - }, - replaceComments(items: FileContextItem[]) { - setStore("context", "items", (current) => [ - ...current.filter((item) => !isCommentItem(item)), - ...items.map((item) => ({ ...item, key: contextItemKey(item) })), - ]) - }, - }, - set: actions.set, - reset: actions.reset, - } -} - -export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({ - name: "Prompt", - gate: false, - init: () => { - const params = useParams() - const cache = new Map() - - const disposeAll = () => { - for (const entry of cache.values()) { - entry.dispose() - } - cache.clear() - } - - onCleanup(disposeAll) - - const prune = () => { - while (cache.size > MAX_PROMPT_SESSIONS) { - const first = cache.keys().next().value - if (!first) return - const entry = cache.get(first) - entry?.dispose() - cache.delete(first) - } - } - - const owner = getOwner() - const load = (dir: string, id: string | undefined) => { - const key = `${dir}:${id ?? WORKSPACE_KEY}` - const existing = cache.get(key) - if (existing) { - cache.delete(key) - cache.set(key, existing) - return existing.value - } - - const entry = createRoot( - (dispose) => ({ - value: createPromptSession(dir, id), - dispose, - }), - owner, - ) - - cache.set(key, entry) - prune() - return entry.value - } - - const session = createMemo(() => load(params.dir!, params.id)) - const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session()) - - return { - ready: () => session().ready, - current: () => session().current(), - cursor: () => session().cursor(), - dirty: () => session().dirty(), - context: { - items: () => session().context.items(), - add: (item: ContextItem) => session().context.add(item), - remove: (key: string) => session().context.remove(key), - removeComment: (path: string, commentID: string) => session().context.removeComment(path, commentID), - updateComment: (path: string, commentID: string, next: Partial & { comment?: string }) => - session().context.updateComment(path, commentID, next), - replaceComments: (items: FileContextItem[]) => session().context.replaceComments(items), - }, - set: (prompt: Prompt, cursorPosition?: number, scope?: Scope) => pick(scope).set(prompt, cursorPosition), - reset: (scope?: Scope) => pick(scope).reset(), - } - }, -}) diff --git a/packages/app/src/context/sdk.tsx b/packages/app/src/context/sdk.tsx deleted file mode 100644 index 2eaf4627c04..00000000000 --- a/packages/app/src/context/sdk.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import type { Event } from "@kilocode/sdk/v2/client" -import { createSimpleContext } from "@opencode-ai/ui/context" -import { createGlobalEmitter } from "@solid-primitives/event-bus" -import { type Accessor, createEffect, createMemo, onCleanup } from "solid-js" -import { useGlobalSDK } from "./global-sdk" - -type SDKEventMap = { - [key in Event["type"]]: Extract -} - -export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ - name: "SDK", - init: (props: { directory: Accessor }) => { - const globalSDK = useGlobalSDK() - - const directory = createMemo(props.directory) - const client = createMemo(() => - globalSDK.createClient({ - directory: directory(), - throwOnError: true, - }), - ) - - const emitter = createGlobalEmitter() - - createEffect(() => { - const unsub = globalSDK.event.on(directory(), (event) => { - emitter.emit(event.type, event) - }) - onCleanup(unsub) - }) - - return { - get directory() { - return directory() - }, - get client() { - return client() - }, - event: emitter, - get url() { - return globalSDK.url - }, - createClient(opts: Parameters[0]) { - return globalSDK.createClient(opts) - }, - } - }, -}) diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx deleted file mode 100644 index 1204fba5571..00000000000 --- a/packages/app/src/context/server.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import { createSimpleContext } from "@opencode-ai/ui/context" -import { type Accessor, batch, createEffect, createMemo, onCleanup } from "solid-js" -import { createStore } from "solid-js/store" -import { Persist, persisted } from "@/utils/persist" -import { useCheckServerHealth } from "@/utils/server-health" - -type StoredProject = { worktree: string; expanded: boolean } -type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http -const HEALTH_POLL_INTERVAL_MS = 10_000 - -export function normalizeServerUrl(input: string) { - const trimmed = input.trim() - if (!trimmed) return - const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `http://${trimmed}` - return withProtocol.replace(/\/+$/, "") -} - -export function serverName(conn?: ServerConnection.Any, ignoreDisplayName = false) { - if (!conn) return "" - if (conn.displayName && !ignoreDisplayName) return conn.displayName - return conn.http.url.replace(/^https?:\/\//, "").replace(/\/+$/, "") -} - -function projectsKey(key: ServerConnection.Key) { - if (!key) return "" - if (key === "sidecar") return "local" - if (isLocalHost(key)) return "local" - return key -} - -function isLocalHost(url: string) { - const host = url.replace(/^https?:\/\//, "").split(":")[0] - if (host === "localhost" || host === "127.0.0.1") return "local" -} - -export namespace ServerConnection { - type Base = { displayName?: string } - - export type HttpBase = { - url: string - username?: string - password?: string - } - - // Regular web connections - export type Http = { - type: "http" - http: HttpBase - } & Base - - export type Sidecar = { - type: "sidecar" - http: HttpBase - } & ( - | // Regular desktop server - { variant: "base" } - // WSL server (windows only) - | { - variant: "wsl" - distro: string - } - ) & - Base - - // Remote server desktop can SSH into - export type Ssh = { - type: "ssh" - host: string - // SSH client exposes an HTTP server for the app to use as a proxy - http: HttpBase - } & Base - - export type Any = - | Http - // All these are desktop-only - | (Sidecar | Ssh) - - export const key = (conn: Any): Key => { - switch (conn.type) { - case "http": - return Key.make(conn.http.url) - case "sidecar": { - if (conn.variant === "wsl") return Key.make(`wsl:${conn.distro}`) - return Key.make("sidecar") - } - case "ssh": - return Key.make(`ssh:${conn.host}`) - } - } - - export type Key = string & { _brand: "Key" } - export const Key = { make: (v: string) => v as Key } -} - -export const { use: useServer, provider: ServerProvider } = createSimpleContext({ - name: "Server", - init: (props: { - defaultServer: ServerConnection.Key - disableHealthCheck?: boolean - servers?: Array - }) => { - const checkServerHealth = useCheckServerHealth() - - const [store, setStore, _, ready] = persisted( - Persist.global("server", ["server.v3"]), - createStore({ - list: [] as StoredServer[], - projects: {} as Record, - lastProject: {} as Record, - }), - ) - - const url = (x: StoredServer) => (typeof x === "string" ? x : "type" in x ? x.http.url : x.url) - - const allServers = createMemo((): Array => { - const servers = [ - ...(props.servers ?? []), - ...store.list.map((value) => - typeof value === "string" - ? { - type: "http" as const, - http: { url: value }, - } - : value, - ), - ] - - const deduped = new Map( - servers.map((value) => { - const conn: ServerConnection.Any = "type" in value ? value : { type: "http", http: value } - return [ServerConnection.key(conn), conn] - }), - ) - - return [...deduped.values()] - }) - - const [state, setState] = createStore({ - active: props.defaultServer, - healthy: undefined as boolean | undefined, - }) - - const healthy = () => state.healthy - - function startHealthPolling(conn: ServerConnection.Any) { - let alive = true - let busy = false - - const run = () => { - if (busy) return - busy = true - void check(conn) - .then((next) => { - if (!alive) return - setState("healthy", next) - }) - .finally(() => { - busy = false - }) - } - - run() - const interval = setInterval(run, HEALTH_POLL_INTERVAL_MS) - return () => { - alive = false - clearInterval(interval) - } - } - - function setActive(input: ServerConnection.Key) { - if (state.active !== input) setState("active", input) - } - - function add(input: ServerConnection.Http) { - const url_ = normalizeServerUrl(input.http.url) - if (!url_) return - const conn = { ...input, http: { ...input.http, url: url_ } } - return batch(() => { - const existing = store.list.findIndex((x) => url(x) === url_) - if (existing !== -1) { - setStore("list", existing, conn) - } else { - setStore("list", store.list.length, conn) - } - setState("active", ServerConnection.key(conn)) - return conn - }) - } - - function remove(key: ServerConnection.Key) { - const list = store.list.filter((x) => url(x) !== key) - batch(() => { - setStore("list", list) - if (state.active === key) { - const next = list[0] - setState("active", next ? ServerConnection.Key.make(url(next)) : props.defaultServer) - } - }) - } - - const isReady = createMemo(() => ready() && !!state.active) - - const check = (conn: ServerConnection.Any) => checkServerHealth(conn.http).then((x) => x.healthy) - - createEffect(() => { - const current_ = current() - if (!current_) return - - if (props.disableHealthCheck) { - setState("healthy", true) - return - } - setState("healthy", undefined) - onCleanup(startHealthPolling(current_)) - }) - - const origin = createMemo(() => projectsKey(state.active)) - const projectsList = createMemo(() => store.projects[origin()] ?? []) - const current: Accessor = createMemo( - () => allServers().find((s) => ServerConnection.key(s) === state.active) ?? allServers()[0], - ) - const isLocal = createMemo(() => { - const c = current() - return (c?.type === "sidecar" && c.variant === "base") || (c?.type === "http" && isLocalHost(c.http.url)) - }) - - return { - ready: isReady, - healthy, - isLocal, - get key() { - return state.active - }, - get name() { - return serverName(current()) - }, - get list() { - return allServers() - }, - get current() { - return current() - }, - setActive, - add, - remove, - projects: { - list: projectsList, - open(directory: string) { - const key = origin() - if (!key) return - const current = store.projects[key] ?? [] - if (current.find((x) => x.worktree === directory)) return - setStore("projects", key, [{ worktree: directory, expanded: true }, ...current]) - }, - close(directory: string) { - const key = origin() - if (!key) return - const current = store.projects[key] ?? [] - setStore( - "projects", - key, - current.filter((x) => x.worktree !== directory), - ) - }, - expand(directory: string) { - const key = origin() - if (!key) return - const current = store.projects[key] ?? [] - const index = current.findIndex((x) => x.worktree === directory) - if (index !== -1) setStore("projects", key, index, "expanded", true) - }, - collapse(directory: string) { - const key = origin() - if (!key) return - const current = store.projects[key] ?? [] - const index = current.findIndex((x) => x.worktree === directory) - if (index !== -1) setStore("projects", key, index, "expanded", false) - }, - move(directory: string, toIndex: number) { - const key = origin() - if (!key) return - const current = store.projects[key] ?? [] - const fromIndex = current.findIndex((x) => x.worktree === directory) - if (fromIndex === -1 || fromIndex === toIndex) return - const result = [...current] - const [item] = result.splice(fromIndex, 1) - result.splice(toIndex, 0, item) - setStore("projects", key, result) - }, - last() { - const key = origin() - if (!key) return - return store.lastProject[key] - }, - touch(directory: string) { - const key = origin() - if (!key) return - setStore("lastProject", key, directory) - }, - }, - } - }, -}) diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx deleted file mode 100644 index be2fb49d7e0..00000000000 --- a/packages/app/src/context/settings.tsx +++ /dev/null @@ -1,332 +0,0 @@ -import { createStore, reconcile } from "solid-js/store" -import { createEffect, createMemo } from "solid-js" -import { createSimpleContext } from "@opencode-ai/ui/context" -import { persisted } from "@/utils/persist" - -export interface NotificationSettings { - agent: boolean - permissions: boolean - errors: boolean -} - -export interface SoundSettings { - agentEnabled: boolean - agent: string - permissionsEnabled: boolean - permissions: string - errorsEnabled: boolean - errors: string -} - -export interface Settings { - general: { - autoSave: boolean - releaseNotes: boolean - followup: "queue" | "steer" - showFileTree: boolean - showNavigation: boolean - showSearch: boolean - showStatus: boolean - showTerminal: boolean - showReasoningSummaries: boolean - shellToolPartsExpanded: boolean - editToolPartsExpanded: boolean - showSessionProgressBar: boolean - } - updates: { - startup: boolean - } - appearance: { - fontSize: number - mono: string - sans: string - terminal: string - } - keybinds: Record - permissions: { - autoApprove: boolean - } - notifications: NotificationSettings - sounds: SoundSettings -} - -export const monoDefault = "System Mono" -export const sansDefault = "System Sans" -export const terminalDefault = "JetBrainsMono Nerd Font Mono" - -const monoFallback = - 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' -const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' -const terminalFallback = - '"JetBrainsMono Nerd Font Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' - -const monoBase = monoFallback -const sansBase = sansFallback -const terminalBase = terminalFallback - -function input(font: string | undefined) { - return font ?? "" -} - -function family(font: string) { - if (/^[\w-]+$/.test(font)) return font - return `"${font.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"` -} - -function stack(font: string | undefined, base: string) { - const value = font?.trim() ?? "" - if (!value) return base - return `${family(value)}, ${base}` -} - -export function monoInput(font: string | undefined) { - return input(font) -} - -export function sansInput(font: string | undefined) { - return input(font) -} - -export function monoFontFamily(font: string | undefined) { - return stack(font, monoBase) -} - -export function sansFontFamily(font: string | undefined) { - return stack(font, sansBase) -} - -export function terminalInput(font: string | undefined) { - return input(font) -} - -export function terminalFontFamily(font: string | undefined) { - return stack(font, terminalBase) -} - -const defaultSettings: Settings = { - general: { - autoSave: true, - releaseNotes: true, - followup: "steer", - showFileTree: false, - showNavigation: false, - showSearch: false, - showStatus: false, - showTerminal: false, - showReasoningSummaries: false, - shellToolPartsExpanded: false, - editToolPartsExpanded: false, - showSessionProgressBar: true, - }, - updates: { - startup: true, - }, - appearance: { - fontSize: 14, - mono: "", - sans: "", - terminal: "", - }, - keybinds: {}, - permissions: { - autoApprove: false, - }, - notifications: { - agent: true, - permissions: true, - errors: false, - }, - sounds: { - agentEnabled: true, - agent: "staplebops-01", - permissionsEnabled: true, - permissions: "staplebops-02", - errorsEnabled: true, - errors: "nope-03", - }, -} - -function withFallback(read: () => T | undefined, fallback: T) { - return createMemo(() => read() ?? fallback) -} - -export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({ - name: "Settings", - init: () => { - const [store, setStore, _, ready] = persisted("settings.v3", createStore(defaultSettings)) - - createEffect(() => { - if (typeof document === "undefined") return - const root = document.documentElement - root.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.mono)) - root.style.setProperty("--font-family-sans", sansFontFamily(store.appearance?.sans)) - }) - - createEffect(() => { - if (store.general?.followup !== "queue") return - setStore("general", "followup", "steer") - }) - - return { - ready, - get current() { - return store - }, - general: { - autoSave: withFallback(() => store.general?.autoSave, defaultSettings.general.autoSave), - setAutoSave(value: boolean) { - setStore("general", "autoSave", value) - }, - releaseNotes: withFallback(() => store.general?.releaseNotes, defaultSettings.general.releaseNotes), - setReleaseNotes(value: boolean) { - setStore("general", "releaseNotes", value) - }, - followup: withFallback( - () => (store.general?.followup === "queue" ? "steer" : store.general?.followup), - defaultSettings.general.followup, - ), - setFollowup(value: "queue" | "steer") { - setStore("general", "followup", value === "queue" ? "steer" : value) - }, - showFileTree: withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree), - setShowFileTree(value: boolean) { - setStore("general", "showFileTree", value) - }, - showNavigation: withFallback(() => store.general?.showNavigation, defaultSettings.general.showNavigation), - setShowNavigation(value: boolean) { - setStore("general", "showNavigation", value) - }, - showSearch: withFallback(() => store.general?.showSearch, defaultSettings.general.showSearch), - setShowSearch(value: boolean) { - setStore("general", "showSearch", value) - }, - showStatus: withFallback(() => store.general?.showStatus, defaultSettings.general.showStatus), - setShowStatus(value: boolean) { - setStore("general", "showStatus", value) - }, - showTerminal: withFallback(() => store.general?.showTerminal, defaultSettings.general.showTerminal), - setShowTerminal(value: boolean) { - setStore("general", "showTerminal", value) - }, - showReasoningSummaries: withFallback( - () => store.general?.showReasoningSummaries, - defaultSettings.general.showReasoningSummaries, - ), - setShowReasoningSummaries(value: boolean) { - setStore("general", "showReasoningSummaries", value) - }, - shellToolPartsExpanded: withFallback( - () => store.general?.shellToolPartsExpanded, - defaultSettings.general.shellToolPartsExpanded, - ), - setShellToolPartsExpanded(value: boolean) { - setStore("general", "shellToolPartsExpanded", value) - }, - editToolPartsExpanded: withFallback( - () => store.general?.editToolPartsExpanded, - defaultSettings.general.editToolPartsExpanded, - ), - setEditToolPartsExpanded(value: boolean) { - setStore("general", "editToolPartsExpanded", value) - }, - showSessionProgressBar: withFallback( - () => store.general?.showSessionProgressBar, - defaultSettings.general.showSessionProgressBar, - ), - setShowSessionProgressBar(value: boolean) { - setStore("general", "showSessionProgressBar", value) - }, - }, - updates: { - startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup), - setStartup(value: boolean) { - setStore("updates", "startup", value) - }, - }, - appearance: { - fontSize: withFallback(() => store.appearance?.fontSize, defaultSettings.appearance.fontSize), - setFontSize(value: number) { - setStore("appearance", "fontSize", value) - }, - font: withFallback(() => store.appearance?.mono, defaultSettings.appearance.mono), - setFont(value: string) { - setStore("appearance", "mono", value.trim() ? value : "") - }, - uiFont: withFallback(() => store.appearance?.sans, defaultSettings.appearance.sans), - setUIFont(value: string) { - setStore("appearance", "sans", value.trim() ? value : "") - }, - terminalFont: withFallback(() => store.appearance?.terminal, defaultSettings.appearance.terminal), - setTerminalFont(value: string) { - setStore("appearance", "terminal", value.trim() ? value : "") - }, - }, - keybinds: { - get: (action: string) => store.keybinds?.[action], - set(action: string, keybind: string) { - setStore("keybinds", action, keybind) - }, - reset(action: string) { - setStore("keybinds", (current) => { - if (!Object.prototype.hasOwnProperty.call(current, action)) return current - const next = { ...current } - delete next[action] - return next - }) - }, - resetAll() { - setStore("keybinds", reconcile({})) - }, - }, - permissions: { - autoApprove: withFallback(() => store.permissions?.autoApprove, defaultSettings.permissions.autoApprove), - setAutoApprove(value: boolean) { - setStore("permissions", "autoApprove", value) - }, - }, - notifications: { - agent: withFallback(() => store.notifications?.agent, defaultSettings.notifications.agent), - setAgent(value: boolean) { - setStore("notifications", "agent", value) - }, - permissions: withFallback(() => store.notifications?.permissions, defaultSettings.notifications.permissions), - setPermissions(value: boolean) { - setStore("notifications", "permissions", value) - }, - errors: withFallback(() => store.notifications?.errors, defaultSettings.notifications.errors), - setErrors(value: boolean) { - setStore("notifications", "errors", value) - }, - }, - sounds: { - agentEnabled: withFallback(() => store.sounds?.agentEnabled, defaultSettings.sounds.agentEnabled), - setAgentEnabled(value: boolean) { - setStore("sounds", "agentEnabled", value) - }, - agent: withFallback(() => store.sounds?.agent, defaultSettings.sounds.agent), - setAgent(value: string) { - setStore("sounds", "agent", value) - }, - permissionsEnabled: withFallback( - () => store.sounds?.permissionsEnabled, - defaultSettings.sounds.permissionsEnabled, - ), - setPermissionsEnabled(value: boolean) { - setStore("sounds", "permissionsEnabled", value) - }, - permissions: withFallback(() => store.sounds?.permissions, defaultSettings.sounds.permissions), - setPermissions(value: string) { - setStore("sounds", "permissions", value) - }, - errorsEnabled: withFallback(() => store.sounds?.errorsEnabled, defaultSettings.sounds.errorsEnabled), - setErrorsEnabled(value: boolean) { - setStore("sounds", "errorsEnabled", value) - }, - errors: withFallback(() => store.sounds?.errors, defaultSettings.sounds.errors), - setErrors(value: string) { - setStore("sounds", "errors", value) - }, - }, - } - }, -}) diff --git a/packages/app/src/context/sync-optimistic.test.ts b/packages/app/src/context/sync-optimistic.test.ts deleted file mode 100644 index 3c1b8f4644d..00000000000 --- a/packages/app/src/context/sync-optimistic.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { Message, Part } from "@kilocode/sdk/v2/client" -import { applyOptimisticAdd, applyOptimisticRemove, mergeOptimisticPage } from "./sync" - -type Text = Extract - -const userMessage = (id: string, sessionID: string): Message => ({ - id, - sessionID, - role: "user", - time: { created: 1 }, - agent: "assistant", - model: { providerID: "openai", modelID: "gpt" }, -}) - -const textPart = (id: string, sessionID: string, messageID: string): Text => ({ - id, - sessionID, - messageID, - type: "text", - text: id, -}) - -describe("sync optimistic reducers", () => { - test("applyOptimisticAdd inserts message in sorted order and stores parts", () => { - const sessionID = "ses_1" - const draft = { - message: { [sessionID]: [userMessage("msg_2", sessionID)] }, - part: {} as Record, - } - - applyOptimisticAdd(draft, { - sessionID, - message: userMessage("msg_1", sessionID), - parts: [textPart("prt_2", sessionID, "msg_1"), textPart("prt_1", sessionID, "msg_1")], - }) - - expect(draft.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2"]) - expect(draft.part.msg_1?.map((x) => x.id)).toEqual(["prt_1", "prt_2"]) - }) - - test("applyOptimisticRemove removes message and part entries", () => { - const sessionID = "ses_1" - const draft = { - message: { [sessionID]: [userMessage("msg_1", sessionID), userMessage("msg_2", sessionID)] }, - part: { - msg_1: [textPart("prt_1", sessionID, "msg_1")], - msg_2: [textPart("prt_2", sessionID, "msg_2")], - } as Record, - } - - applyOptimisticRemove(draft, { sessionID, messageID: "msg_1" }) - - expect(draft.message[sessionID]?.map((x) => x.id)).toEqual(["msg_2"]) - expect(draft.part.msg_1).toBeUndefined() - expect(draft.part.msg_2).toHaveLength(1) - }) - - test("mergeOptimisticPage keeps pending messages in fetched timelines", () => { - const sessionID = "ses_1" - const page = mergeOptimisticPage( - { - session: [userMessage("msg_1", sessionID)], - part: [{ id: "msg_1", part: [textPart("prt_1", sessionID, "msg_1")] }], - complete: true, - }, - [{ message: userMessage("msg_2", sessionID), parts: [textPart("prt_2", sessionID, "msg_2")] }], - ) - - expect(page.session.map((x) => x.id)).toEqual(["msg_1", "msg_2"]) - expect(page.part.find((x) => x.id === "msg_2")?.part.map((x) => x.id)).toEqual(["prt_2"]) - expect(page.confirmed).toEqual([]) - expect(page.complete).toBe(true) - }) - - test("mergeOptimisticPage keeps missing optimistic parts until the server has them", () => { - const sessionID = "ses_1" - const page = mergeOptimisticPage( - { - session: [userMessage("msg_2", sessionID)], - part: [{ id: "msg_2", part: [textPart("prt_2", sessionID, "msg_2")] }], - complete: true, - }, - [ - { - message: userMessage("msg_2", sessionID), - parts: [textPart("prt_1", sessionID, "msg_2"), textPart("prt_2", sessionID, "msg_2")], - }, - ], - ) - - expect(page.part.find((x) => x.id === "msg_2")?.part.map((x) => x.id)).toEqual(["prt_1", "prt_2"]) - expect(page.confirmed).toEqual([]) - }) - - test("mergeOptimisticPage confirms echoed messages once all parts arrive", () => { - const sessionID = "ses_1" - const page = mergeOptimisticPage( - { - session: [userMessage("msg_2", sessionID)], - part: [ - { - id: "msg_2", - part: [{ ...textPart("prt_1", sessionID, "msg_2"), text: "server" }, textPart("prt_2", sessionID, "msg_2")], - }, - ], - complete: true, - }, - [ - { - message: userMessage("msg_2", sessionID), - parts: [textPart("prt_1", sessionID, "msg_2"), textPart("prt_2", sessionID, "msg_2")], - }, - ], - ) - - expect(page.confirmed).toEqual(["msg_2"]) - expect(page.part.find((x) => x.id === "msg_2")?.part).toMatchObject([ - { id: "prt_1", type: "text", text: "server" }, - { id: "prt_2", type: "text", text: "prt_2" }, - ]) - }) -}) diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx deleted file mode 100644 index 3fb37e4b26a..00000000000 --- a/packages/app/src/context/sync.tsx +++ /dev/null @@ -1,619 +0,0 @@ -import { batch, createMemo } from "solid-js" -import { createStore, produce, reconcile } from "solid-js/store" -import { Binary } from "@opencode-ai/shared/util/binary" -import { retry } from "@opencode-ai/shared/util/retry" -import { createSimpleContext } from "@opencode-ai/ui/context" -import { - clearSessionPrefetch, - getSessionPrefetch, - getSessionPrefetchPromise, - setSessionPrefetch, -} from "./global-sync/session-prefetch" -import { useGlobalSync } from "./global-sync" -import { useSDK } from "./sdk" -import type { Message, Part } from "@kilocode/sdk/v2/client" -import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache" -import { diffs as list, message as clean } from "@/utils/diffs" - -const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) - -function sortParts(parts: Part[]) { - return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)) -} - -function runInflight(map: Map>, key: string, task: () => Promise) { - const pending = map.get(key) - if (pending) return pending - const promise = task().finally(() => { - map.delete(key) - }) - map.set(key, promise) - return promise -} - -const keyFor = (directory: string, id: string) => `${directory}\n${id}` - -const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) - -function merge(a: readonly T[], b: readonly T[]) { - const map = new Map(a.map((item) => [item.id, item] as const)) - for (const item of b) map.set(item.id, item) - return [...map.values()].sort((x, y) => cmp(x.id, y.id)) -} - -type OptimisticStore = { - message: Record - part: Record -} - -type OptimisticAddInput = { - sessionID: string - message: Message - parts: Part[] -} - -type OptimisticRemoveInput = { - sessionID: string - messageID: string -} - -type OptimisticItem = { - message: Message - parts: Part[] -} - -type MessagePage = { - session: Message[] - part: { id: string; part: Part[] }[] - cursor?: string - complete: boolean -} - -const hasParts = (parts: Part[] | undefined, want: Part[]) => { - if (!parts) return want.length === 0 - return want.every((part) => Binary.search(parts, part.id, (item) => item.id).found) -} - -const mergeParts = (parts: Part[] | undefined, want: Part[]) => { - if (!parts) return sortParts(want) - const next = [...parts] - let changed = false - for (const part of want) { - const result = Binary.search(next, part.id, (item) => item.id) - if (result.found) continue - next.splice(result.index, 0, part) - changed = true - } - if (!changed) return parts - return next -} - -export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) { - if (items.length === 0) return { ...page, confirmed: [] as string[] } - - const session = [...page.session] - const part = new Map(page.part.map((item) => [item.id, sortParts(item.part)])) - const confirmed: string[] = [] - - for (const item of items) { - const result = Binary.search(session, item.message.id, (message) => message.id) - const found = result.found - if (!found) session.splice(result.index, 0, item.message) - - const current = part.get(item.message.id) - if (found && hasParts(current, item.parts)) { - confirmed.push(item.message.id) - continue - } - - part.set(item.message.id, mergeParts(current, item.parts)) - } - - return { - cursor: page.cursor, - complete: page.complete, - session, - part: [...part.entries()].sort((a, b) => cmp(a[0], b[0])).map(([id, part]) => ({ id, part })), - confirmed, - } -} - -export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) { - const messages = draft.message[input.sessionID] - if (messages) { - const result = Binary.search(messages, input.message.id, (m) => m.id) - messages.splice(result.index, 0, input.message) - } else { - draft.message[input.sessionID] = [input.message] - } - draft.part[input.message.id] = sortParts(input.parts) -} - -export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticRemoveInput) { - const messages = draft.message[input.sessionID] - if (messages) { - const result = Binary.search(messages, input.messageID, (m) => m.id) - if (result.found) messages.splice(result.index, 1) - } - delete draft.part[input.messageID] -} - -function setOptimisticAdd(setStore: (...args: unknown[]) => void, input: OptimisticAddInput) { - setStore("message", input.sessionID, (messages: Message[] | undefined) => { - if (!messages) return [input.message] - const result = Binary.search(messages, input.message.id, (m) => m.id) - const next = [...messages] - next.splice(result.index, 0, input.message) - return next - }) - setStore("part", input.message.id, sortParts(input.parts)) -} - -function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: OptimisticRemoveInput) { - setStore("message", input.sessionID, (messages: Message[] | undefined) => { - if (!messages) return messages - const result = Binary.search(messages, input.messageID, (m) => m.id) - if (!result.found) return messages - const next = [...messages] - next.splice(result.index, 1) - return next - }) - setStore("part", (part: Record) => { - if (!(input.messageID in part)) return part - const next = { ...part } - delete next[input.messageID] - return next - }) -} - -export const { use: useSync, provider: SyncProvider } = createSimpleContext({ - name: "Sync", - init: () => { - const globalSync = useGlobalSync() - const sdk = useSDK() - - type Child = ReturnType<(typeof globalSync)["child"]> - type Setter = Child[1] - - const current = createMemo(() => globalSync.child(sdk.directory)) - const target = (directory?: string) => { - if (!directory || directory === sdk.directory) return current() - return globalSync.child(directory) - } - const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/") - const initialMessagePageSize = 80 - const historyMessagePageSize = 200 - const inflight = new Map>() - const inflightDiff = new Map>() - const inflightTodo = new Map>() - const optimistic = new Map>() - const maxDirs = 30 - const seen = new Map>() - const [meta, setMeta] = createStore({ - limit: {} as Record, - cursor: {} as Record, - complete: {} as Record, - loading: {} as Record, - }) - - const getSession = (sessionID: string) => { - const store = current()[0] - const match = Binary.search(store.session, sessionID, (s) => s.id) - if (match.found) return store.session[match.index] - return undefined - } - - const setOptimistic = (directory: string, sessionID: string, item: OptimisticItem) => { - const key = keyFor(directory, sessionID) - const list = optimistic.get(key) - if (list) { - list.set(item.message.id, { message: item.message, parts: sortParts(item.parts) }) - return - } - optimistic.set(key, new Map([[item.message.id, { message: item.message, parts: sortParts(item.parts) }]])) - } - - const clearOptimistic = (directory: string, sessionID: string, messageID?: string) => { - const key = keyFor(directory, sessionID) - if (!messageID) { - optimistic.delete(key) - return - } - - const list = optimistic.get(key) - if (!list) return - list.delete(messageID) - if (list.size === 0) optimistic.delete(key) - } - - const getOptimistic = (directory: string, sessionID: string) => [ - ...(optimistic.get(keyFor(directory, sessionID))?.values() ?? []), - ] - - const seenFor = (directory: string) => { - const existing = seen.get(directory) - if (existing) { - seen.delete(directory) - seen.set(directory, existing) - return existing - } - const created = new Set() - seen.set(directory, created) - while (seen.size > maxDirs) { - const first = seen.keys().next().value - if (!first) break - const stale = [...(seen.get(first) ?? [])] - seen.delete(first) - const [, setStore] = globalSync.child(first, { bootstrap: false }) - evict(first, setStore, stale) - } - return created - } - - const clearMeta = (directory: string, sessionIDs: string[]) => { - if (sessionIDs.length === 0) return - for (const sessionID of sessionIDs) { - clearOptimistic(directory, sessionID) - } - setMeta( - produce((draft) => { - for (const sessionID of sessionIDs) { - const key = keyFor(directory, sessionID) - delete draft.limit[key] - delete draft.cursor[key] - delete draft.complete[key] - delete draft.loading[key] - } - }), - ) - } - - const evict = (directory: string, setStore: Setter, sessionIDs: string[]) => { - if (sessionIDs.length === 0) return - clearSessionPrefetch(directory, sessionIDs) - for (const sessionID of sessionIDs) { - globalSync.todo.set(sessionID, undefined) - } - setStore( - produce((draft) => { - dropSessionCaches(draft, sessionIDs) - }), - ) - clearMeta(directory, sessionIDs) - } - - const touch = (directory: string, setStore: Setter, sessionID: string) => { - const stale = pickSessionCacheEvictions({ - seen: seenFor(directory), - keep: sessionID, - limit: SESSION_CACHE_LIMIT, - }) - evict(directory, setStore, stale) - } - - const fetchMessages = async (input: { - client: typeof sdk.client - sessionID: string - limit: number - before?: string - }) => { - const messages = await retry(() => - input.client.session.messages({ sessionID: input.sessionID, limit: input.limit, before: input.before }), - ) - const items = (messages.data ?? []).filter((x) => !!x?.info?.id) - const session = items.map((x) => clean(x.info)).sort((a, b) => cmp(a.id, b.id)) - const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) })) - const cursor = messages.response.headers.get("x-next-cursor") ?? undefined - return { - session, - part, - cursor, - complete: !cursor, - } - } - - const tracked = (directory: string, sessionID: string) => seen.get(directory)?.has(sessionID) ?? false - - const loadMessages = async (input: { - directory: string - client: typeof sdk.client - setStore: Setter - sessionID: string - limit: number - before?: string - mode?: "replace" | "prepend" - }) => { - const key = keyFor(input.directory, input.sessionID) - if (meta.loading[key]) return - - setMeta("loading", key, true) - await fetchMessages(input) - .then((page) => { - if (!tracked(input.directory, input.sessionID)) return - const next = mergeOptimisticPage(page, getOptimistic(input.directory, input.sessionID)) - for (const messageID of next.confirmed) { - clearOptimistic(input.directory, input.sessionID, messageID) - } - const [store] = globalSync.child(input.directory, { bootstrap: false }) - const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : [] - const message = input.mode === "prepend" ? merge(cached, next.session) : next.session - batch(() => { - input.setStore("message", input.sessionID, reconcile(message, { key: "id" })) - for (const p of next.part) { - const filtered = p.part.filter((x) => !SKIP_PARTS.has(x.type)) - if (filtered.length) input.setStore("part", p.id, filtered) - } - setMeta("limit", key, message.length) - setMeta("cursor", key, next.cursor) - setMeta("complete", key, next.complete) - setSessionPrefetch({ - directory: input.directory, - sessionID: input.sessionID, - limit: message.length, - cursor: next.cursor, - complete: next.complete, - }) - }) - }) - .finally(() => { - setMeta( - produce((draft) => { - if (!tracked(input.directory, input.sessionID)) { - delete draft.loading[key] - return - } - draft.loading[key] = false - }), - ) - }) - } - - return { - get data() { - return current()[0] - }, - get set(): Setter { - return current()[1] - }, - get status() { - return current()[0].status - }, - get ready() { - return current()[0].status !== "loading" - }, - get project() { - const store = current()[0] - const match = Binary.search(globalSync.data.project, store.project, (p) => p.id) - if (match.found) return globalSync.data.project[match.index] - return undefined - }, - session: { - get: getSession, - optimistic: { - add(input: { directory?: string; sessionID: string; message: Message; parts: Part[] }) { - const directory = input.directory ?? sdk.directory - const [, setStore] = target(input.directory) - setOptimistic(directory, input.sessionID, { message: input.message, parts: input.parts }) - setOptimisticAdd(setStore as (...args: unknown[]) => void, input) - }, - remove(input: { directory?: string; sessionID: string; messageID: string }) { - const directory = input.directory ?? sdk.directory - const [, setStore] = target(input.directory) - clearOptimistic(directory, input.sessionID, input.messageID) - setOptimisticRemove(setStore as (...args: unknown[]) => void, input) - }, - }, - addOptimisticMessage(input: { - sessionID: string - messageID: string - parts: Part[] - agent: string - model: { providerID: string; modelID: string } - variant?: string - }) { - const message: Message = { - id: input.messageID, - sessionID: input.sessionID, - role: "user", - time: { created: Date.now() }, - agent: input.agent, - model: { ...input.model, variant: input.variant }, - } - const [, setStore] = target() - setOptimistic(sdk.directory, input.sessionID, { message, parts: input.parts }) - setOptimisticAdd(setStore as (...args: unknown[]) => void, { - sessionID: input.sessionID, - message, - parts: input.parts, - }) - }, - async sync(sessionID: string, opts?: { force?: boolean }) { - const directory = sdk.directory - const client = sdk.client - const [store, setStore] = globalSync.child(directory) - const key = keyFor(directory, sessionID) - - touch(directory, setStore, sessionID) - - const seeded = getSessionPrefetch(directory, sessionID) - if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) { - batch(() => { - setMeta("limit", key, seeded.limit) - setMeta("cursor", key, seeded.cursor) - setMeta("complete", key, seeded.complete) - setMeta("loading", key, false) - }) - } - - return runInflight(inflight, key, async () => { - const pending = getSessionPrefetchPromise(directory, sessionID) - if (pending) { - await pending - const seeded = getSessionPrefetch(directory, sessionID) - if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) { - batch(() => { - setMeta("limit", key, seeded.limit) - setMeta("cursor", key, seeded.cursor) - setMeta("complete", key, seeded.complete) - setMeta("loading", key, false) - }) - } - } - - const hasSession = Binary.search(store.session, sessionID, (s) => s.id).found - const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined - if (cached && hasSession && !opts?.force) return - - const limit = meta.limit[key] ?? initialMessagePageSize - const sessionReq = - hasSession && !opts?.force - ? Promise.resolve() - : retry(() => client.session.get({ sessionID })).then((session) => { - if (!tracked(directory, sessionID)) return - const data = session.data - if (!data) return - setStore( - "session", - produce((draft) => { - const match = Binary.search(draft, sessionID, (s) => s.id) - if (match.found) { - draft[match.index] = data - return - } - draft.splice(match.index, 0, data) - }), - ) - }) - - const messagesReq = - cached && !opts?.force - ? Promise.resolve() - : loadMessages({ - directory, - client, - setStore, - sessionID, - limit, - }) - - await Promise.all([sessionReq, messagesReq]) - }) - }, - async diff(sessionID: string, opts?: { force?: boolean }) { - const directory = sdk.directory - const client = sdk.client - const [store, setStore] = globalSync.child(directory) - touch(directory, setStore, sessionID) - if (store.session_diff[sessionID] !== undefined && !opts?.force) return - - const key = keyFor(directory, sessionID) - return runInflight(inflightDiff, key, () => - retry(() => client.session.diff({ sessionID })).then((diff) => { - if (!tracked(directory, sessionID)) return - setStore("session_diff", sessionID, reconcile(list(diff.data), { key: "file" })) - }), - ) - }, - async todo(sessionID: string, opts?: { force?: boolean }) { - const directory = sdk.directory - const client = sdk.client - const [store, setStore] = globalSync.child(directory) - touch(directory, setStore, sessionID) - const existing = store.todo[sessionID] - const cached = globalSync.data.session_todo[sessionID] - if (existing !== undefined) { - if (cached === undefined) { - globalSync.todo.set(sessionID, existing) - } - if (!opts?.force) return - } - - if (cached !== undefined) { - setStore("todo", sessionID, reconcile(cached, { key: "id" })) - } - - const key = keyFor(directory, sessionID) - return runInflight(inflightTodo, key, () => - retry(() => client.session.todo({ sessionID })).then((todo) => { - if (!tracked(directory, sessionID)) return - const list = todo.data ?? [] - setStore("todo", sessionID, reconcile(list, { key: "id" })) - globalSync.todo.set(sessionID, list) - }), - ) - }, - history: { - more(sessionID: string) { - const store = current()[0] - const key = keyFor(sdk.directory, sessionID) - if (store.message[sessionID] === undefined) return false - if (meta.limit[key] === undefined) return false - if (meta.complete[key]) return false - return !!meta.cursor[key] - }, - loading(sessionID: string) { - const key = keyFor(sdk.directory, sessionID) - return meta.loading[key] ?? false - }, - async loadMore(sessionID: string, count?: number) { - const directory = sdk.directory - const client = sdk.client - const [, setStore] = globalSync.child(directory) - touch(directory, setStore, sessionID) - const key = keyFor(directory, sessionID) - const step = count ?? historyMessagePageSize - if (meta.loading[key]) return - if (meta.complete[key]) return - const before = meta.cursor[key] - if (!before) return - - await loadMessages({ - directory, - client, - setStore, - sessionID, - limit: step, - before, - mode: "prepend", - }) - }, - }, - evict(sessionID: string, directory = sdk.directory) { - const [, setStore] = globalSync.child(directory) - seenFor(directory).delete(sessionID) - evict(directory, setStore, [sessionID]) - }, - fetch: async (count = 10) => { - const directory = sdk.directory - const client = sdk.client - const [store, setStore] = globalSync.child(directory) - setStore("limit", (x) => x + count) - await client.session.list().then((x) => { - const sessions = (x.data ?? []) - .filter((s) => !!s?.id) - .sort((a, b) => cmp(a.id, b.id)) - .slice(0, store.limit) - setStore("session", reconcile(sessions, { key: "id" })) - }) - }, - more: createMemo(() => current()[0].session.length >= current()[0].limit), - archive: async (sessionID: string) => { - const directory = sdk.directory - const client = sdk.client - const [, setStore] = globalSync.child(directory) - await client.session.update({ sessionID, time: { archived: Date.now() } }) - setStore( - produce((draft) => { - const match = Binary.search(draft.session, sessionID, (s) => s.id) - if (match.found) draft.session.splice(match.index, 1) - }), - ) - }, - }, - absolute, - get directory() { - return current()[0].path.directory - }, - } - }, -}) diff --git a/packages/app/src/context/terminal-title.ts b/packages/app/src/context/terminal-title.ts deleted file mode 100644 index c8b18f42119..00000000000 --- a/packages/app/src/context/terminal-title.ts +++ /dev/null @@ -1,24 +0,0 @@ -const template = "Terminal {{number}}" - -const numbered = [ - template, - "محطة طرفية {{number}}", - "Терминал {{number}}", - "ターミナル {{number}}", - "터미널 {{number}}", - "เทอร์มินัล {{number}}", - "终端 {{number}}", - "終端機 {{number}}", -] - -export function defaultTitle(number: number) { - return template.replace("{{number}}", String(number)) -} - -export function isDefaultTitle(title: string, number: number) { - return numbered.some((text) => title === text.replace("{{number}}", String(number))) -} - -export function titleNumber(title: string, max: number) { - return Array.from({ length: max }, (_, idx) => idx + 1).find((number) => isDefaultTitle(title, number)) -} diff --git a/packages/app/src/context/terminal.test.ts b/packages/app/src/context/terminal.test.ts deleted file mode 100644 index 6e07e031241..00000000000 --- a/packages/app/src/context/terminal.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { beforeAll, describe, expect, mock, test } from "bun:test" - -let getWorkspaceTerminalCacheKey: (dir: string) => string -let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => string[] -let migrateTerminalState: (value: unknown) => unknown - -beforeAll(async () => { - mock.module("@solidjs/router", () => ({ - useNavigate: () => () => undefined, - useParams: () => ({}), - })) - mock.module("@opencode-ai/ui/context", () => ({ - createSimpleContext: () => ({ - use: () => undefined, - provider: () => undefined, - }), - })) - const mod = await import("./terminal") - getWorkspaceTerminalCacheKey = mod.getWorkspaceTerminalCacheKey - getLegacyTerminalStorageKeys = mod.getLegacyTerminalStorageKeys - migrateTerminalState = mod.migrateTerminalState -}) - -describe("getWorkspaceTerminalCacheKey", () => { - test("uses workspace-only directory cache key", () => { - expect(getWorkspaceTerminalCacheKey("/repo")).toBe("/repo:__workspace__") - }) -}) - -describe("getLegacyTerminalStorageKeys", () => { - test("keeps workspace storage path when no legacy session id", () => { - expect(getLegacyTerminalStorageKeys("/repo")).toEqual(["/repo/terminal.v1"]) - }) - - test("includes legacy session path before workspace path", () => { - expect(getLegacyTerminalStorageKeys("/repo", "session-123")).toEqual([ - "/repo/terminal/session-123.v1", - "/repo/terminal.v1", - ]) - }) -}) - -describe("migrateTerminalState", () => { - test("drops invalid terminals and restores a valid active terminal", () => { - expect( - migrateTerminalState({ - active: "missing", - all: [ - null, - { id: "one", title: "Terminal 2" }, - { id: "one", title: "duplicate", titleNumber: 9 }, - { id: "two", title: "logs", titleNumber: 4, rows: 24, cols: 80 }, - { title: "no-id" }, - ], - }), - ).toEqual({ - active: "one", - all: [ - { id: "one", title: "Terminal 2", titleNumber: 2 }, - { id: "two", title: "logs", titleNumber: 4, rows: 24, cols: 80 }, - ], - }) - }) - - test("keeps a valid active id", () => { - expect( - migrateTerminalState({ - active: "two", - all: [ - { id: "one", title: "Terminal 1" }, - { id: "two", title: "shell", titleNumber: 7 }, - ], - }), - ).toEqual({ - active: "two", - all: [ - { id: "one", title: "Terminal 1", titleNumber: 1 }, - { id: "two", title: "shell", titleNumber: 7 }, - ], - }) - }) -}) diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx deleted file mode 100644 index 31d2d6e04ca..00000000000 --- a/packages/app/src/context/terminal.tsx +++ /dev/null @@ -1,437 +0,0 @@ -import { createStore, produce } from "solid-js/store" -import { createSimpleContext } from "@opencode-ai/ui/context" -import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "solid-js" -import { useParams } from "@solidjs/router" -import { useSDK } from "./sdk" -import type { Platform } from "./platform" -import { defaultTitle, titleNumber } from "./terminal-title" -import { Persist, persisted, removePersisted } from "@/utils/persist" - -export type LocalPTY = { - id: string - title: string - titleNumber: number - rows?: number - cols?: number - buffer?: string - scrollY?: number - cursor?: number -} - -const WORKSPACE_KEY = "__workspace__" -const MAX_TERMINAL_SESSIONS = 20 - -function record(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value) -} - -function text(value: unknown) { - return typeof value === "string" ? value : undefined -} - -function num(value: unknown) { - return typeof value === "number" && Number.isFinite(value) ? value : undefined -} - -function numberFromTitle(title: string) { - return titleNumber(title, MAX_TERMINAL_SESSIONS) -} - -function pty(value: unknown): LocalPTY | undefined { - if (!record(value)) return - - const id = text(value.id) - if (!id) return - - const title = text(value.title) ?? "" - const number = num(value.titleNumber) - const rows = num(value.rows) - const cols = num(value.cols) - const buffer = text(value.buffer) - const scrollY = num(value.scrollY) - const cursor = num(value.cursor) - - return { - id, - title, - titleNumber: number && number > 0 ? number : (numberFromTitle(title) ?? 0), - ...(rows !== undefined ? { rows } : {}), - ...(cols !== undefined ? { cols } : {}), - ...(buffer !== undefined ? { buffer } : {}), - ...(scrollY !== undefined ? { scrollY } : {}), - ...(cursor !== undefined ? { cursor } : {}), - } -} - -export function migrateTerminalState(value: unknown) { - if (!record(value)) return value - - const seen = new Set() - const all = (Array.isArray(value.all) ? value.all : []).flatMap((item) => { - const next = pty(item) - if (!next || seen.has(next.id)) return [] - seen.add(next.id) - return [next] - }) - - const active = text(value.active) - - return { - active: active && seen.has(active) ? active : all[0]?.id, - all, - } -} - -export function getWorkspaceTerminalCacheKey(dir: string) { - return `${dir}:${WORKSPACE_KEY}` -} - -export function getLegacyTerminalStorageKeys(dir: string, legacySessionID?: string) { - if (!legacySessionID) return [`${dir}/terminal.v1`] - return [`${dir}/terminal/${legacySessionID}.v1`, `${dir}/terminal.v1`] -} - -type TerminalSession = ReturnType - -type TerminalCacheEntry = { - value: TerminalSession - dispose: VoidFunction -} - -const caches = new Set>() - -const trimTerminal = (pty: LocalPTY) => { - if (!pty.buffer && pty.cursor === undefined && pty.scrollY === undefined) return pty - return { - ...pty, - buffer: undefined, - cursor: undefined, - scrollY: undefined, - } -} - -export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform) { - const key = getWorkspaceTerminalCacheKey(dir) - for (const cache of caches) { - const entry = cache.get(key) - entry?.value.clear() - } - - void removePersisted(Persist.workspace(dir, "terminal"), platform) - - const legacy = new Set(getLegacyTerminalStorageKeys(dir)) - for (const id of sessionIDs ?? []) { - for (const key of getLegacyTerminalStorageKeys(dir, id)) { - legacy.add(key) - } - } - for (const key of legacy) { - void removePersisted({ key }, platform) - } -} - -function createWorkspaceTerminalSession(sdk: ReturnType, dir: string, legacySessionID?: string) { - const legacy = getLegacyTerminalStorageKeys(dir, legacySessionID) - - const [store, setStore, _, ready] = persisted( - { - ...Persist.workspace(dir, "terminal", legacy), - migrate: migrateTerminalState, - }, - createStore<{ - active?: string - all: LocalPTY[] - }>({ - all: [], - }), - ) - - const pickNextTerminalNumber = () => { - const existingTitleNumbers = new Set( - store.all.flatMap((pty) => { - const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined - if (direct !== undefined) return [direct] - const parsed = numberFromTitle(pty.title) - if (parsed === undefined) return [] - return [parsed] - }), - ) - - return ( - Array.from({ length: existingTitleNumbers.size + 1 }, (_, index) => index + 1).find( - (number) => !existingTitleNumbers.has(number), - ) ?? 1 - ) - } - - const removeExited = (id: string) => { - const all = store.all - const index = all.findIndex((x) => x.id === id) - if (index === -1) return - const active = store.active === id ? (index === 0 ? all[1]?.id : all[0]?.id) : store.active - batch(() => { - setStore("active", active) - setStore( - "all", - produce((draft) => { - draft.splice(index, 1) - }), - ) - }) - } - - const unsub = sdk.event.on("pty.exited", (event: { properties: { id: string } }) => { - removeExited(event.properties.id) - }) - onCleanup(unsub) - - const update = (client: ReturnType["client"], pty: Partial & { id: string }) => { - const index = store.all.findIndex((x) => x.id === pty.id) - const previous = index >= 0 ? store.all[index] : undefined - if (index >= 0) { - setStore("all", index, (item) => ({ ...item, ...pty })) - } - client.pty - .update({ - ptyID: pty.id, - title: pty.title, - size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined, - }) - .catch((error: unknown) => { - if (previous) { - const currentIndex = store.all.findIndex((item) => item.id === pty.id) - if (currentIndex >= 0) setStore("all", currentIndex, previous) - } - console.error("Failed to update terminal", error) - }) - } - - const clone = async (client: ReturnType["client"], id: string) => { - const index = store.all.findIndex((x) => x.id === id) - const pty = store.all[index] - if (!pty) return - const next = await client.pty - .create({ - title: pty.title, - }) - .catch((error: unknown) => { - console.error("Failed to clone terminal", error) - return undefined - }) - if (!next?.data) return - - const active = store.active === pty.id - - batch(() => { - setStore("all", index, { - id: next.data.id, - title: next.data.title ?? pty.title, - titleNumber: pty.titleNumber, - buffer: undefined, - cursor: undefined, - scrollY: undefined, - rows: undefined, - cols: undefined, - }) - if (active) { - setStore("active", next.data.id) - } - }) - } - - return { - ready, - all: createMemo(() => store.all), - active: createMemo(() => store.active), - clear() { - batch(() => { - setStore("active", undefined) - setStore("all", []) - }) - }, - new() { - const nextNumber = pickNextTerminalNumber() - - sdk.client.pty - .create({ title: defaultTitle(nextNumber) }) - .then((pty: { data?: { id?: string; title?: string } }) => { - const id = pty.data?.id - if (!id) return - const newTerminal = { - id, - title: pty.data?.title ?? defaultTitle(nextNumber), - titleNumber: nextNumber, - } - setStore("all", store.all.length, newTerminal) - setStore("active", id) - }) - .catch((error: unknown) => { - console.error("Failed to create terminal", error) - }) - }, - update(pty: Partial & { id: string }) { - update(sdk.client, pty) - }, - trim(id: string) { - const index = store.all.findIndex((x) => x.id === id) - if (index === -1) return - setStore("all", index, (pty) => trimTerminal(pty)) - }, - trimAll() { - setStore("all", (all) => { - const next = all.map(trimTerminal) - if (next.every((pty, index) => pty === all[index])) return all - return next - }) - }, - async clone(id: string) { - await clone(sdk.client, id) - }, - bind() { - const client = sdk.client - return { - trim(id: string) { - const index = store.all.findIndex((x) => x.id === id) - if (index === -1) return - setStore("all", index, (pty) => trimTerminal(pty)) - }, - update(pty: Partial & { id: string }) { - update(client, pty) - }, - async clone(id: string) { - await clone(client, id) - }, - } - }, - open(id: string) { - setStore("active", id) - }, - next() { - const index = store.all.findIndex((x) => x.id === store.active) - if (index === -1) return - const nextIndex = (index + 1) % store.all.length - setStore("active", store.all[nextIndex]?.id) - }, - previous() { - const index = store.all.findIndex((x) => x.id === store.active) - if (index === -1) return - const prevIndex = index === 0 ? store.all.length - 1 : index - 1 - setStore("active", store.all[prevIndex]?.id) - }, - async close(id: string) { - const index = store.all.findIndex((f) => f.id === id) - if (index !== -1) { - batch(() => { - if (store.active === id) { - const next = index > 0 ? store.all[index - 1]?.id : store.all[1]?.id - setStore("active", next) - } - setStore( - "all", - produce((all) => { - all.splice(index, 1) - }), - ) - }) - } - - await sdk.client.pty.remove({ ptyID: id }).catch((error: unknown) => { - console.error("Failed to close terminal", error) - }) - }, - move(id: string, to: number) { - const index = store.all.findIndex((f) => f.id === id) - if (index === -1) return - setStore( - "all", - produce((all) => { - all.splice(to, 0, all.splice(index, 1)[0]) - }), - ) - }, - } -} - -export const { use: useTerminal, provider: TerminalProvider } = createSimpleContext({ - name: "Terminal", - gate: false, - init: () => { - const sdk = useSDK() - const params = useParams() - const cache = new Map() - - caches.add(cache) - onCleanup(() => caches.delete(cache)) - - const disposeAll = () => { - for (const entry of cache.values()) { - entry.dispose() - } - cache.clear() - } - - onCleanup(disposeAll) - - const prune = () => { - while (cache.size > MAX_TERMINAL_SESSIONS) { - const first = cache.keys().next().value - if (!first) return - const entry = cache.get(first) - entry?.dispose() - cache.delete(first) - } - } - - const loadWorkspace = (dir: string, legacySessionID?: string) => { - // Terminals are workspace-scoped so tabs persist while switching sessions in the same directory. - const key = getWorkspaceTerminalCacheKey(dir) - const existing = cache.get(key) - if (existing) { - cache.delete(key) - cache.set(key, existing) - return existing.value - } - - const entry = createRoot((dispose) => ({ - value: createWorkspaceTerminalSession(sdk, dir, legacySessionID), - dispose, - })) - - cache.set(key, entry) - prune() - return entry.value - } - - const workspace = createMemo(() => loadWorkspace(params.dir!, params.id)) - - createEffect( - on( - () => ({ dir: params.dir, id: params.id }), - (next, prev) => { - if (!prev?.dir) return - if (next.dir === prev.dir && next.id === prev.id) return - if (next.dir === prev.dir && next.id) return - loadWorkspace(prev.dir, prev.id).trimAll() - }, - { defer: true }, - ), - ) - - return { - ready: () => workspace().ready(), - all: () => workspace().all(), - active: () => workspace().active(), - new: () => workspace().new(), - update: (pty: Partial & { id: string }) => workspace().update(pty), - trim: (id: string) => workspace().trim(id), - trimAll: () => workspace().trimAll(), - clone: (id: string) => workspace().clone(id), - bind: () => workspace(), - open: (id: string) => workspace().open(id), - close: (id: string) => workspace().close(id), - move: (id: string, to: number) => workspace().move(id, to), - next: () => workspace().next(), - previous: () => workspace().previous(), - } - }, -}) diff --git a/packages/app/src/custom-elements.d.ts b/packages/app/src/custom-elements.d.ts deleted file mode 100644 index ce6acfabc34..00000000000 --- a/packages/app/src/custom-elements.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// kilocode_change start - keep Windows checkouts parseable when symlinks are disabled. -import "../../ui/src/custom-elements" -// kilocode_change end diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx deleted file mode 100644 index 84b2b799dad..00000000000 --- a/packages/app/src/entry.tsx +++ /dev/null @@ -1,144 +0,0 @@ -// @refresh reload - -import { render } from "solid-js/web" -import { AppBaseProviders, AppInterface } from "@/app" -import { type Platform, PlatformProvider } from "@/context/platform" -import { dict as en } from "@/i18n/en" -import { dict as zh } from "@/i18n/zh" -import { handleNotificationClick } from "@/utils/notification-click" -import pkg from "../package.json" -import { ServerConnection } from "./context/server" - -const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl" - -const getLocale = () => { - if (typeof navigator !== "object") return "en" as const - const languages = navigator.languages?.length ? navigator.languages : [navigator.language] - for (const language of languages) { - if (!language) continue - if (language.toLowerCase().startsWith("zh")) return "zh" as const - } - return "en" as const -} - -const getRootNotFoundError = () => { - const key = "error.dev.rootNotFound" as const - const locale = getLocale() - return locale === "zh" ? (zh[key] ?? en[key]) : en[key] -} - -const getStorage = (key: string) => { - if (typeof localStorage === "undefined") return null - try { - return localStorage.getItem(key) - } catch { - return null - } -} - -const setStorage = (key: string, value: string | null) => { - if (typeof localStorage === "undefined") return - try { - if (value !== null) { - localStorage.setItem(key, value) - return - } - localStorage.removeItem(key) - } catch { - return - } -} - -const readDefaultServerUrl = () => getStorage(DEFAULT_SERVER_URL_KEY) -const writeDefaultServerUrl = (url: string | null) => setStorage(DEFAULT_SERVER_URL_KEY, url) - -const notify: Platform["notify"] = async (title, description, href) => { - if (!("Notification" in window)) return - - const permission = - Notification.permission === "default" - ? await Notification.requestPermission().catch(() => "denied") - : Notification.permission - - if (permission !== "granted") return - - const inView = document.visibilityState === "visible" && document.hasFocus() - if (inView) return - - const notification = new Notification(title, { - body: description ?? "", - icon: "https://opencode.ai/favicon-96x96-v3.png", - }) - - notification.onclick = () => { - handleNotificationClick(href) - notification.close() - } -} - -const openLink: Platform["openLink"] = (url) => { - window.open(url, "_blank") -} - -const back: Platform["back"] = () => { - window.history.back() -} - -const forward: Platform["forward"] = () => { - window.history.forward() -} - -const restart: Platform["restart"] = async () => { - window.location.reload() -} - -const root = document.getElementById("root") -if (!(root instanceof HTMLElement) && import.meta.env.DEV) { - throw new Error(getRootNotFoundError()) -} - -const getCurrentUrl = () => { - if (location.hostname.includes("opencode.ai")) return "http://localhost:4096" - if (import.meta.env.DEV) - return `http://${import.meta.env.VITE_KILO_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_KILO_SERVER_PORT ?? "4096"}` - return location.origin -} - -const getDefaultUrl = () => { - const lsDefault = readDefaultServerUrl() - if (lsDefault) return lsDefault - return getCurrentUrl() -} - -const platform: Platform = { - platform: "web", - version: pkg.version, - openLink, - back, - forward, - restart, - notify, - getDefaultServer: async () => { - const stored = readDefaultServerUrl() - return stored ? ServerConnection.Key.make(stored) : null - }, - setDefaultServer: writeDefaultServerUrl, -} - -if (root instanceof HTMLElement) { - const server: ServerConnection.Http = { type: "http", http: { url: getCurrentUrl() } } - render( - () => ( - - - - - - ), - root, - ) -} diff --git a/packages/app/src/env.d.ts b/packages/app/src/env.d.ts deleted file mode 100644 index 1197383a3cd..00000000000 --- a/packages/app/src/env.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -interface ImportMetaEnv { - readonly VITE_KILO_SERVER_HOST: string - readonly VITE_KILO_SERVER_PORT: string - readonly VITE_KILO_CHANNEL?: "dev" | "beta" | "prod" -} - -interface ImportMeta { - readonly env: ImportMetaEnv -} - -export declare module "solid-js" { - namespace JSX { - interface Directives { - sortable: true - } - } -} diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts deleted file mode 100644 index 95260fc766d..00000000000 --- a/packages/app/src/hooks/use-providers.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useGlobalSync } from "@/context/global-sync" -import { decode64 } from "@/utils/base64" -import { useParams } from "@solidjs/router" -import { createMemo } from "solid-js" - -// kilocode_change start - Preferred providers list (order determines display priority) -export const popularProviders = ["kilo", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"] -// kilocode_change end -const popularProviderSet = new Set(popularProviders) - -export function useProviders() { - const globalSync = useGlobalSync() - const params = useParams() - const dir = createMemo(() => decode64(params.dir) ?? "") - const providers = () => { - if (dir()) { - const [projectStore] = globalSync.child(dir()) - if (projectStore.provider_ready) return projectStore.provider - } - return globalSync.data.provider - } - return { - all: () => providers().all, - default: () => providers().default, - popular: () => providers().all.filter((p) => popularProviderSet.has(p.id)), - connected: () => { - const connected = new Set(providers().connected) - return providers().all.filter((p) => connected.has(p.id)) - }, - paid: () => { - const connected = new Set(providers().connected) - return providers().all.filter( - (p) => connected.has(p.id) && (p.id !== "opencode" || Object.values(p.models).some((m) => m.cost?.input)), - ) - }, - } -} diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts deleted file mode 100644 index 47f98dbbfe6..00000000000 --- a/packages/app/src/i18n/ar.ts +++ /dev/null @@ -1,845 +0,0 @@ -export const dict = { - "command.category.suggested": "مقترح", - "command.category.view": "عرض", - "command.category.project": "مشروع", - "command.category.provider": "موفر", - "command.category.server": "خادم", - "command.category.session": "جلسة", - "command.category.theme": "سمة", - "command.category.language": "لغة", - "command.category.file": "ملف", - "command.category.context": "سياق", - "command.category.terminal": "محطة طرفية", - "command.category.model": "نموذج", - "command.category.mcp": "MCP", - "command.category.agent": "وكيل", - "command.category.permissions": "أذونات", - "command.category.workspace": "مساحة عمل", - "command.category.settings": "إعدادات", - "theme.scheme.system": "نظام", - "theme.scheme.light": "فاتح", - "theme.scheme.dark": "داكن", - "command.sidebar.toggle": "تبديل الشريط الجانبي", - "command.project.open": "فتح مشروع", - "command.provider.connect": "اتصال بموفر", - "command.server.switch": "تبديل الخادم", - "command.settings.open": "فتح الإعدادات", - "command.session.previous": "الجلسة السابقة", - "command.session.next": "الجلسة التالية", - "command.session.previous.unseen": "الجلسة غير المقروءة السابقة", - "command.session.next.unseen": "الجلسة غير المقروءة التالية", - "command.session.archive": "أرشفة الجلسة", - "command.palette": "لوحة الأوامر", - "command.theme.cycle": "تغيير السمة", - "command.theme.set": "استخدام السمة: {{theme}}", - "command.theme.scheme.cycle": "تغيير مخطط الألوان", - "command.theme.scheme.set": "استخدام مخطط الألوان: {{scheme}}", - "command.language.cycle": "تغيير اللغة", - "command.language.set": "استخدام اللغة: {{language}}", - "command.session.new": "جلسة جديدة", - "command.file.open": "فتح ملف", - "command.tab.close": "إغلاق علامة التبويب", - "command.context.addSelection": "إضافة التحديد إلى السياق", - "command.context.addSelection.description": "إضافة الأسطر المحددة من الملف الحالي", - "command.input.focus": "التركيز على حقل الإدخال", - "command.terminal.toggle": "تبديل المحطة الطرفية", - "command.fileTree.toggle": "تبديل شجرة الملفات", - "command.review.toggle": "تبديل المراجعة", - "command.terminal.new": "محطة طرفية جديدة", - "command.terminal.new.description": "إنشاء علامة تبويب جديدة للمحطة الطرفية", - "command.steps.toggle": "تبديل الخطوات", - "command.steps.toggle.description": "إظهار أو إخفاء خطوات الرسالة الحالية", - "command.message.previous": "الرسالة السابقة", - "command.message.previous.description": "انتقل إلى رسالة المستخدم السابقة", - "command.message.next": "الرسالة التالية", - "command.message.next.description": "انتقل إلى رسالة المستخدم التالية", - "command.model.choose": "اختيار نموذج", - "command.model.choose.description": "حدد نموذجًا مختلفًا", - "command.mcp.toggle": "تبديل MCPs", - "command.mcp.toggle.description": "تبديل MCPs", - "command.agent.cycle": "تغيير الوكيل", - "command.agent.cycle.description": "التبديل إلى الوكيل التالي", - "command.agent.cycle.reverse": "تغيير الوكيل للخلف", - "command.agent.cycle.reverse.description": "التبديل إلى الوكيل السابق", - "command.model.variant.cycle": "تغيير جهد التفكير", - "command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي", - "command.prompt.mode.shell": "Shell", - "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "قبول الأذونات تلقائيًا", - "command.permissions.autoaccept.disable": "إيقاف قبول الأذونات تلقائيًا", - "command.workspace.toggle": "تبديل مساحات العمل", - "command.workspace.toggle.description": "تمكين أو تعطيل مساحات العمل المتعددة في الشريط الجانبي", - "command.session.undo": "تراجع", - "command.session.undo.description": "تراجع عن الرسالة الأخيرة", - "command.session.redo": "إعادة", - "command.session.redo.description": "إعادة الرسالة التي تم التراجع عنها", - "command.session.compact": "ضغط الجلسة", - "command.session.compact.description": "تلخيص الجلسة لتقليل حجم السياق", - "command.session.fork": "تشعب من الرسالة", - "command.session.fork.description": "إنشاء جلسة جديدة من رسالة سابقة", - "command.session.share": "مشاركة الجلسة", - "command.session.share.description": "مشاركة هذه الجلسة ونسخ الرابط إلى الحافظة", - "command.session.unshare": "إلغاء مشاركة الجلسة", - "command.session.unshare.description": "إيقاف مشاركة هذه الجلسة", - "palette.search.placeholder": "البحث في الملفات والأوامر والجلسات", - "palette.empty": "لا توجد نتائج", - "palette.group.commands": "الأوامر", - "palette.group.files": "الملفات", - "dialog.provider.search.placeholder": "البحث عن موفرين", - "dialog.provider.empty": "لم يتم العثور على موفرين", - "dialog.provider.group.popular": "شائع", - "dialog.provider.group.other": "آخر", - "dialog.provider.tag.recommended": "موصى به", - "dialog.provider.opencode.note": "نماذج مختارة تتضمن Claude و GPT و Gemini والمزيد", - "dialog.provider.opencode.tagline": "نماذج موثوقة ومحسنة", - "dialog.provider.opencodeGo.tagline": "اشتراك منخفض التكلفة للجميع", - "dialog.provider.anthropic.note": "اتصل باستخدام Claude Pro/Max أو مفتاح API", - "dialog.provider.copilot.note": "اتصل باستخدام Copilot أو مفتاح API", - "dialog.provider.openai.note": "اتصل باستخدام ChatGPT Pro/Plus أو مفتاح API", - "dialog.provider.google.note": "نماذج Gemini لاستجابات سريعة ومنظمة", - "dialog.provider.openrouter.note": "الوصول إلى جميع النماذج المدعومة من موفر واحد", - "dialog.provider.vercel.note": "وصول موحد إلى نماذج الذكاء الاصطناعي مع توجيه ذكي", - "dialog.model.select.title": "تحديد نموذج", - "dialog.model.search.placeholder": "البحث عن نماذج", - "dialog.model.empty": "لا توجد نتائج للنماذج", - "dialog.model.manage": "إدارة النماذج", - "dialog.model.manage.description": "تخصيص النماذج التي تظهر في محدد النماذج.", - "dialog.model.manage.provider.toggle": "تبديل جميع نماذج {{provider}}", - "dialog.model.unpaid.freeModels.title": "نماذج مجانية مقدمة من Kilo", - "dialog.model.unpaid.addMore.title": "إضافة المزيد من النماذج من موفرين مشهورين", - "dialog.provider.viewAll": "عرض المزيد من الموفرين", - "provider.connect.title": "اتصال {{provider}}", - "provider.connect.title.anthropicProMax": "تسجيل الدخول باستخدام Claude Pro/Max", - "provider.connect.selectMethod": "حدد طريقة تسجيل الدخول لـ {{provider}}.", - "provider.connect.method.apiKey": "مفتاح API", - "provider.connect.status.inProgress": "جارٍ التفويض...", - "provider.connect.status.waiting": "في انتظار التفويض...", - "provider.connect.status.failed": "فشل التفويض: {{error}}", - "provider.connect.apiKey.description": - "أدخل مفتاح واجهة برمجة تطبيقات {{provider}} الخاص بك لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.", - "provider.connect.apiKey.label": "مفتاح واجهة برمجة تطبيقات {{provider}}", - "provider.connect.apiKey.placeholder": "مفتاح API", - "provider.connect.apiKey.required": "مفتاح API مطلوب", - "provider.connect.opencodeZen.line1": - "يمنحك OpenCode Zen الوصول إلى مجموعة مختارة من النماذج الموثوقة والمحسنة لوكلاء البرمجة.", - "provider.connect.opencodeZen.line2": - "باستخدام مفتاح API واحد، ستحصل على إمكانية الوصول إلى نماذج مثل Claude و GPT و Gemini و GLM والمزيد.", - "provider.connect.opencodeZen.visit.prefix": "قم بزيارة ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " للحصول على مفتاح API الخاص بك.", - "provider.connect.oauth.code.visit.prefix": "قم بزيارة ", - "provider.connect.oauth.code.visit.link": "هذا الرابط", - "provider.connect.oauth.code.visit.suffix": - " للحصول على رمز التفويض الخاص بك لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.", - "provider.connect.oauth.code.label": "رمز تفويض {{method}}", - "provider.connect.oauth.code.placeholder": "رمز التفويض", - "provider.connect.oauth.code.required": "رمز التفويض مطلوب", - "provider.connect.oauth.code.invalid": "رمز التفويض غير صالح", - "provider.connect.oauth.auto.visit.prefix": "قم بزيارة ", - "provider.connect.oauth.auto.visit.link": "هذا الرابط", - "provider.connect.oauth.auto.visit.suffix": " وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.", - "provider.connect.oauth.auto.confirmationCode": "رمز التأكيد", - "provider.connect.toast.connected.title": "تم توصيل {{provider}}", - "provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.", - "provider.custom.title": "موفر مخصص", - "provider.custom.description.prefix": "تكوين موفر متوافق مع OpenAI. راجع ", - "provider.custom.description.link": "وثائق تكوين الموفر", - "provider.custom.description.suffix": ".", - "provider.custom.field.providerID.label": "معرف الموفر", - "provider.custom.field.providerID.placeholder": "myprovider", - "provider.custom.field.providerID.description": "أحرف صغيرة، أرقام، شرطات، أو شرطات سفلية", - "provider.custom.field.name.label": "اسم العرض", - "provider.custom.field.name.placeholder": "موفر الذكاء الاصطناعي الخاص بي", - "provider.custom.field.baseURL.label": "عنوان URL الأساسي", - "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", - "provider.custom.field.apiKey.label": "مفتاح API", - "provider.custom.field.apiKey.placeholder": "مفتاح API", - "provider.custom.field.apiKey.description": "اختياري. اتركه فارغًا إذا كنت تدير المصادقة عبر الترويسات.", - "provider.custom.models.label": "النماذج", - "provider.custom.models.id.label": "المعرف", - "provider.custom.models.id.placeholder": "model-id", - "provider.custom.models.name.label": "الاسم", - "provider.custom.models.name.placeholder": "اسم العرض", - "provider.custom.models.remove": "إزالة النموذج", - "provider.custom.models.add": "إضافة نموذج", - "provider.custom.headers.label": "الترويسات (اختياري)", - "provider.custom.headers.key.label": "ترويسة", - "provider.custom.headers.key.placeholder": "Header-Name", - "provider.custom.headers.value.label": "القيمة", - "provider.custom.headers.value.placeholder": "قيمة", - "provider.custom.headers.remove": "إزالة الترويسة", - "provider.custom.headers.add": "إضافة ترويسة", - "provider.custom.error.providerID.required": "معرف الموفر مطلوب", - "provider.custom.error.providerID.format": "استخدم أحرفًا صغيرة، أرقامًا، شرطات، أو شرطات سفلية", - "provider.custom.error.providerID.exists": "معرف الموفر هذا موجود بالفعل", - "provider.custom.error.name.required": "اسم العرض مطلوب", - "provider.custom.error.baseURL.required": "عنوان URL الأساسي مطلوب", - "provider.custom.error.baseURL.format": "يجب أن يبدأ بـ http:// أو https://", - "provider.custom.error.required": "مطلوب", - "provider.custom.error.duplicate": "مكرر", - "provider.disconnect.toast.disconnected.title": "تم فصل {{provider}}", - "provider.disconnect.toast.disconnected.description": "لم تعد نماذج {{provider}} متاحة.", - "model.tag.free": "مجاني", - "model.tag.latest": "الأحدث", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "نص", - "model.input.image": "صورة", - "model.input.audio": "صوت", - "model.input.video": "فيديو", - "model.input.pdf": "pdf", - "model.tooltip.allows": "يسمح: {{inputs}}", - "model.tooltip.reasoning.allowed": "يسمح بالاستنتاج", - "model.tooltip.reasoning.none": "بدون استنتاج", - "model.tooltip.context": "حد السياق {{limit}}", - "common.search.placeholder": "بحث", - "common.goBack": "رجوع", - "common.goForward": "انتقل للأمام", - "common.loading": "جارٍ التحميل", - "common.loading.ellipsis": "...", - "common.cancel": "إلغاء", - "common.connect": "اتصال", - "common.disconnect": "قطع الاتصال", - "common.continue": "إرسال", - "common.submit": "إرسال", - "common.save": "حفظ", - "common.saving": "جارٍ الحفظ...", - "common.default": "افتراضي", - "common.attachment": "مرفق", - "prompt.placeholder.shell": "أدخل أمر shell... {{example}}", - "prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"', - "prompt.placeholder.simple": "اسأل أي شيء...", - "prompt.placeholder.summarizeComments": "لخّص التعليقات…", - "prompt.placeholder.summarizeComment": "لخّص التعليق…", - "prompt.mode.shell": "Shell", - "prompt.mode.normal": "Prompt", - "prompt.mode.shell.exit": "esc للخروج", - "prompt.example.1": "إصلاح TODO في قاعدة التعليمات البرمجية", - "prompt.example.2": "ما هو المكدس التقني لهذا المشروع؟", - "prompt.example.3": "إصلاح الاختبارات المعطلة", - "prompt.example.4": "اشرح كيف تعمل المصادقة", - "prompt.example.5": "البحث عن وإصلاح الثغرات الأمنية", - "prompt.example.6": "إضافة اختبارات وحدة لخدمة المستخدم", - "prompt.example.7": "إعادة هيكلة هذه الدالة لتكون أكثر قابلية للقراءة", - "prompt.example.8": "ماذا يعني هذا الخطأ؟", - "prompt.example.9": "ساعدني في تصحيح هذه المشكلة", - "prompt.example.10": "توليد وثائق API", - "prompt.example.11": "تحسين استعلامات قاعدة البيانات", - "prompt.example.12": "إضافة التحقق من صحة الإدخال", - "prompt.example.13": "إنشاء مكون جديد لـ...", - "prompt.example.14": "كيف أقوم بنشر هذا المشروع؟", - "prompt.example.15": "مراجعة الكود الخاص بي لأفضل الممارسات", - "prompt.example.16": "إضافة معالجة الأخطاء لهذه الدالة", - "prompt.example.17": "اشرح نمط regex هذا", - "prompt.example.18": "تحويل هذا إلى TypeScript", - "prompt.example.19": "إضافة تسجيل الدخول (logging) في جميع أنحاء قاعدة التعليمات البرمجية", - "prompt.example.20": "ما هي التبعيات القديمة؟", - "prompt.example.21": "ساعدني في كتابة برنامج نصي للهجرة", - "prompt.example.22": "تنفيذ التخزين المؤقت لهذه النقطة النهائية", - "prompt.example.23": "إضافة ترقيم الصفحات إلى هذه القائمة", - "prompt.example.24": "إنشاء أمر CLI لـ...", - "prompt.example.25": "كيف تعمل متغيرات البيئة هنا؟", - "prompt.popover.emptyResults": "لا توجد نتائج مطابقة", - "prompt.popover.emptyCommands": "لا توجد أوامر مطابقة", - "prompt.dropzone.label": "أفلت الصور أو ملفات PDF أو الملفات النصية هنا", - "prompt.dropzone.file.label": "أفلت لإشارة @ للملف", - "prompt.slash.badge.custom": "مخصص", - "prompt.slash.badge.skill": "مهارة", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "نشط", - "prompt.context.includeActiveFile": "تضمين الملف النشط", - "prompt.context.removeActiveFile": "إزالة الملف النشط من السياق", - "prompt.context.removeFile": "إزالة الملف من السياق", - "prompt.action.attachFile": "إرفاق ملف", - "prompt.attachment.remove": "إزالة المرفق", - "prompt.action.send": "إرسال", - "prompt.action.stop": "توقف", - "prompt.toast.pasteUnsupported.title": "مرفق غير مدعوم", - "prompt.toast.pasteUnsupported.description": "يمكن إرفاق الصور أو ملفات PDF أو الملفات النصية فقط هنا.", - "prompt.toast.modelAgentRequired.title": "حدد وكيلاً ونموذجاً", - "prompt.toast.modelAgentRequired.description": "اختر وكيلاً ونموذجاً قبل إرسال الموجه.", - "prompt.toast.worktreeCreateFailed.title": "فشل إنشاء شجرة العمل", - "prompt.toast.sessionCreateFailed.title": "فشل إنشاء الجلسة", - "prompt.toast.shellSendFailed.title": "فشل إرسال أمر shell", - "prompt.toast.commandSendFailed.title": "فشل إرسال الأمر", - "prompt.toast.promptSendFailed.title": "فشل إرسال الموجه", - "prompt.toast.promptSendFailed.description": "تعذر استرداد الجلسة", - "dialog.mcp.title": "MCPs", - "dialog.mcp.description": "{{enabled}} من {{total}} مفعل", - "dialog.mcp.empty": "لم يتم تكوين MCPs", - "dialog.lsp.empty": "تم الكشف تلقائيًا عن LSPs من أنواع الملفات", - "dialog.plugins.empty": "الإضافات المكونة في opencode.json", - "mcp.status.connected": "متصل", - "mcp.status.failed": "فشل", - "mcp.status.needs_auth": "يحتاج إلى مصادقة", - "mcp.status.disabled": "معطل", - "dialog.fork.empty": "لا توجد رسائل للتفرع منها", - "dialog.directory.search.placeholder": "البحث في المجلدات", - "dialog.directory.empty": "لم يتم العثور على مجلدات", - "dialog.server.title": "الخوادم", - "dialog.server.description": "تبديل خادم Kilo الذي يتصل به هذا التطبيق.", - "dialog.server.search.placeholder": "البحث في الخوادم", - "dialog.server.empty": "لا توجد خوادم بعد", - "dialog.server.add.title": "إضافة خادم", - "dialog.server.add.url": "عنوان URL للخادم", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "تعذر الاتصال بالخادم", - "dialog.server.add.checking": "جارٍ التحقق...", - "dialog.server.add.button": "إضافة خادم", - "dialog.server.add.name": "اسم الخادم (اختياري)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "اسم المستخدم (اختياري)", - "dialog.server.add.password": "كلمة المرور (اختياري)", - "dialog.server.edit.title": "تحرير الخادم", - "dialog.server.default.title": "الخادم الافتراضي", - "dialog.server.default.description": - "الاتصال بهذا الخادم عند بدء تشغيل التطبيق بدلاً من بدء خادم محلي. يتطلب إعادة التشغيل.", - "dialog.server.default.none": "لم يتم تحديد خادم", - "dialog.server.default.set": "تعيين الخادم الحالي كافتراضي", - "dialog.server.default.clear": "مسح", - "dialog.server.action.remove": "إزالة الخادم", - "dialog.server.menu.edit": "تعديل", - "dialog.server.menu.default": "تعيين كافتراضي", - "dialog.server.menu.defaultRemove": "إزالة الافتراضي", - "dialog.server.menu.delete": "حذف", - "dialog.server.current": "الخادم الحالي", - "dialog.server.status.default": "افتراضي", - "dialog.project.edit.title": "تحرير المشروع", - "dialog.project.edit.name": "الاسم", - "dialog.project.edit.icon": "أيقونة", - "dialog.project.edit.icon.alt": "أيقونة المشروع", - "dialog.project.edit.icon.hint": "انقر أو اسحب صورة", - "dialog.project.edit.icon.recommended": "موصى به: 128x128px", - "dialog.project.edit.color": "لون", - "dialog.project.edit.color.select": "اختر لون {{color}}", - "dialog.project.edit.worktree.startup": "سكريبت بدء تشغيل مساحة العمل", - "dialog.project.edit.worktree.startup.description": "يتم تشغيله بعد إنشاء مساحة عمل جديدة (شجرة عمل).", - "dialog.project.edit.worktree.startup.placeholder": "مثال: bun install", - "context.breakdown.title": "تفصيل السياق", - "context.breakdown.note": 'تفصيل تقريبي لرموز الإدخال. يشمل "أخرى" تعريفات الأدوات والنفقات العامة.', - "context.breakdown.system": "النظام", - "context.breakdown.user": "المستخدم", - "context.breakdown.assistant": "المساعد", - "context.breakdown.tool": "استدعاءات الأداة", - "context.breakdown.other": "أخرى", - "context.systemPrompt.title": "موجه النظام", - "context.rawMessages.title": "الرسائل الخام", - "context.stats.session": "جلسة", - "context.stats.messages": "رسائل", - "context.stats.provider": "موفر", - "context.stats.model": "نموذج", - "context.stats.limit": "حد السياق", - "context.stats.totalTokens": "إجمالي الرموز", - "context.stats.usage": "استخدام", - "context.stats.inputTokens": "رموز الإدخال", - "context.stats.outputTokens": "رموز الإخراج", - "context.stats.reasoningTokens": "رموز الاستنتاج", - "context.stats.cacheTokens": "رموز التخزين المؤقت (قراءة/كتابة)", - "context.stats.userMessages": "رسائل المستخدم", - "context.stats.assistantMessages": "رسائل المساعد", - "context.stats.totalCost": "التكلفة الإجمالية", - "context.stats.sessionCreated": "تم إنشاء الجلسة", - "context.stats.lastActivity": "آخر نشاط", - "context.usage.tokens": "رموز", - "context.usage.usage": "استخدام", - "context.usage.cost": "تكلفة", - "context.usage.clickToView": "انقر لعرض السياق", - "context.usage.view": "عرض استخدام السياق", - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - "toast.language.title": "لغة", - "toast.language.description": "تم التبديل إلى {{language}}", - "toast.theme.title": "تم تبديل السمة", - "toast.scheme.title": "مخطط الألوان", - "toast.workspace.enabled.title": "تم تمكين مساحات العمل", - "toast.workspace.enabled.description": "الآن يتم عرض عدة worktrees في الشريط الجانبي", - "toast.workspace.disabled.title": "تم تعطيل مساحات العمل", - "toast.workspace.disabled.description": "يتم عرض worktree الرئيسي فقط في الشريط الجانبي", - "toast.permissions.autoaccept.on.title": "يتم قبول الأذونات تلقائيًا", - "toast.permissions.autoaccept.on.description": "ستتم الموافقة على طلبات الأذونات تلقائيًا", - "toast.permissions.autoaccept.off.title": "تم إيقاف قبول الأذونات تلقائيًا", - "toast.permissions.autoaccept.off.description": "ستتطلب طلبات الأذونات موافقة", - "toast.model.none.title": "لم يتم تحديد نموذج", - "toast.model.none.description": "قم بتوصيل موفر لتلخيص هذه الجلسة", - "toast.file.loadFailed.title": "فشل تحميل الملف", - "toast.file.listFailed.title": "فشل سرد الملفات", - "toast.context.noLineSelection.title": "لا يوجد تحديد للأسطر", - "toast.context.noLineSelection.description": "حدد نطاق أسطر في تبويب ملف أولاً.", - "toast.session.share.copyFailed.title": "فشل نسخ عنوان URL إلى الحافظة", - "toast.session.share.success.title": "تمت مشاركة الجلسة", - "toast.session.share.success.description": "تم نسخ عنوان URL للمشاركة إلى الحافظة!", - "toast.session.share.failed.title": "فشل مشاركة الجلسة", - "toast.session.share.failed.description": "حدث خطأ أثناء مشاركة الجلسة", - "toast.session.unshare.success.title": "تم إلغاء مشاركة الجلسة", - "toast.session.unshare.success.description": "تم إلغاء مشاركة الجلسة بنجاح!", - "toast.session.unshare.failed.title": "فشل إلغاء مشاركة الجلسة", - "toast.session.unshare.failed.description": "حدث خطأ أثناء إلغاء مشاركة الجلسة", - "toast.session.listFailed.title": "فشل تحميل الجلسات لـ {{project}}", - "toast.update.title": "تحديث متاح", - "toast.update.description": "نسخة جديدة من Kilo ({{version}}) متاحة الآن للتثبيت.", - "toast.update.action.installRestart": "تثبيت وإعادة تشغيل", - "toast.update.action.notYet": "ليس الآن", - "error.page.title": "حدث خطأ ما", - "error.page.description": "حدث خطأ أثناء تحميل التطبيق.", - "error.page.details.label": "تفاصيل الخطأ", - "error.page.action.restart": "إعادة تشغيل", - "error.page.action.checking": "جارٍ التحقق...", - "error.page.action.checkUpdates": "التحقق من وجود تحديثات", - "error.page.action.updateTo": "تحديث إلى {{version}}", - "error.page.report.prefix": "يرجى الإبلاغ عن هذا الخطأ لفريق Kilo", - "error.page.report.discord": "على Discord", - "error.page.version": "الإصدار: {{version}}", - "error.dev.rootNotFound": - "لم يتم العثور على العنصر الجذري. هل نسيت إضافته إلى index.html؟ أو ربما تمت كتابة سمة id بشكل خاطئ؟", - "error.globalSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟", - "directory.error.invalidUrl": "دليل غير صالح في عنوان URL.", - "error.chain.unknown": "خطأ غير معروف", - "error.chain.causedBy": "بسبب:", - "error.chain.apiError": "خطأ API", - "error.chain.status": "الحالة: {{status}}", - "error.chain.retryable": "قابل لإعادة المحاولة: {{retryable}}", - "error.chain.responseBody": "نص الاستجابة:\n{{body}}", - "error.chain.didYouMean": "هل كنت تعني: {{suggestions}}", - "error.chain.modelNotFound": "النموذج غير موجود: {{provider}}/{{model}}", - "error.chain.checkConfig": "تحقق من أسماء الموفر/النموذج في التكوين (opencode.json)", - "error.chain.mcpFailed": 'فشل خادم MCP "{{name}}". لاحظ أن Kilo لا يدعم مصادقة MCP بعد.', - "error.chain.providerAuthFailed": "فشلت مصادقة الموفر ({{provider}}): {{message}}", - "error.chain.providerInitFailed": 'فشل تهيئة الموفر "{{provider}}". تحقق من بيانات الاعتماد والتكوين.', - "error.chain.configJsonInvalid": "ملف التكوين في {{path}} ليس JSON(C) صالحًا", - "error.chain.configJsonInvalidWithMessage": "ملف التكوين في {{path}} ليس JSON(C) صالحًا: {{message}}", - "error.chain.configDirectoryTypo": - 'الدليل "{{dir}}" في {{path}} غير صالح. أعد تسمية الدليل إلى "{{suggestion}}" أو قم بإزالته. هذا خطأ مطبعي شائع.', - "error.chain.configFrontmatterError": "فشل تحليل frontmatter في {{path}}:\n{{message}}", - "error.chain.configInvalid": "ملف التكوين في {{path}} غير صالح", - "error.chain.configInvalidWithMessage": "ملف التكوين في {{path}} غير صالح: {{message}}", - "notification.permission.title": "مطلوب إذن", - "notification.permission.description": "{{sessionTitle}} في {{projectName}} يحتاج إلى إذن", - "notification.question.title": "سؤال", - "notification.question.description": "{{sessionTitle}} في {{projectName}} لديه سؤال", - "notification.action.goToSession": "انتقل إلى الجلسة", - "notification.session.responseReady.title": "الاستجابة جاهزة", - "notification.session.error.title": "خطأ في الجلسة", - "notification.session.error.fallbackDescription": "حدث خطأ", - "home.recentProjects": "المشاريع الحديثة", - "home.empty.title": "لا توجد مشاريع حديثة", - "home.empty.description": "ابدأ بفتح مشروع محلي", - "session.tab.session": "جلسة", - "session.tab.review": "مراجعة", - "session.tab.context": "سياق", - "session.panel.reviewAndFiles": "المراجعة والملفات", - "session.review.filesChanged": "تم تغيير {{count}} ملفات", - "session.review.change.one": "تغيير", - "session.review.change.other": "تغييرات", - "session.review.loadingChanges": "جارٍ تحميل التغييرات...", - "session.review.empty": "لا توجد تغييرات في هذه الجلسة بعد", - "session.review.noChanges": "لا توجد تغييرات", - "session.review.noVcs": "لم يتم اكتشاف نظام التحكم في الإصدار Git، لن يتم عرض التغييرات", - "session.review.noSnapshot": "تم تعطيل تتبع اللقطات في التكوين، لذا فإن تغييرات الجلسة غير متوفرة", - "session.files.selectToOpen": "اختر ملفًا لفتحه", - "session.files.all": "كل الملفات", - "session.files.empty": "لا توجد ملفات", - "session.files.binaryContent": "ملف ثنائي (لا يمكن عرض المحتوى)", - "session.messages.renderEarlier": "عرض الرسائل السابقة", - "session.messages.loadingEarlier": "جارٍ تحميل الرسائل السابقة...", - "session.messages.loadEarlier": "تحميل الرسائل السابقة", - "session.messages.loading": "جارٍ تحميل الرسائل...", - "session.messages.jumpToLatest": "الانتقال إلى الأحدث", - "session.context.addToContext": "إضافة {{selection}} إلى السياق", - "session.todo.title": "المهام", - "session.todo.collapse": "طي", - "session.todo.expand": "توسيع", - "session.followupDock.summary.one": "{{count}} رسالة في الانتظار", - "session.followupDock.summary.other": "{{count}} رسائل في الانتظار", - "session.followupDock.sendNow": "إرسال الآن", - "session.followupDock.edit": "تحرير", - "session.followupDock.collapse": "طي الرسائل المنتظرة", - "session.followupDock.expand": "توسيع الرسائل المنتظرة", - "session.revertDock.summary.one": "{{count}} رسالة تم التراجع عنها", - "session.revertDock.summary.other": "{{count}} رسائل تم التراجع عنها", - "session.revertDock.collapse": "طي الرسائل التي تم التراجع عنها", - "session.revertDock.expand": "توسيع الرسائل التي تم التراجع عنها", - "session.revertDock.restore": "استعادة الرسالة", - "session.new.title": "ابنِ أي شيء", - "session.new.worktree.main": "الفرع الرئيسي", - "session.new.worktree.mainWithBranch": "الفرع الرئيسي ({{branch}})", - "session.new.worktree.create": "إنشاء شجرة عمل جديدة", - "session.new.lastModified": "آخر تعديل", - "session.header.search.placeholder": "بحث {{project}}", - "session.header.searchFiles": "بحث عن الملفات", - "session.header.openIn": "فتح في", - "session.header.open.action": "فتح {{app}}", - "session.header.open.ariaLabel": "فتح في {{app}}", - "session.header.open.menu": "خيارات الفتح", - "session.header.open.copyPath": "نسخ المسار", - "status.popover.trigger": "الحالة", - "status.popover.ariaLabel": "إعدادات الخوادم", - "status.popover.tab.servers": "الخوادم", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "الإضافات", - "status.popover.action.manageServers": "إدارة الخوادم", - "session.share.popover.title": "نشر على الويب", - "session.share.popover.description.shared": "هذه الجلسة عامة على الويب. يمكن لأي شخص لديه الرابط الوصول إليها.", - "session.share.popover.description.unshared": "شارك الجلسة علنًا على الويب. ستكون متاحة لأي شخص لديه الرابط.", - "session.share.action.share": "مشاركة", - "session.share.action.publish": "نشر", - "session.share.action.publishing": "جارٍ النشر...", - "session.share.action.unpublish": "إلغاء النشر", - "session.share.action.unpublishing": "جارٍ إلغاء النشر...", - "session.share.action.view": "عرض", - "session.share.copy.copied": "تم النسخ", - "session.share.copy.copyLink": "نسخ الرابط", - "lsp.tooltip.none": "لا توجد خوادم LSP", - "lsp.label.connected": "{{count}} LSP", - "prompt.loading": "جارٍ تحميل الموجه...", - "terminal.loading": "جارٍ تحميل المحطة الطرفية...", - "terminal.title": "محطة طرفية", - "terminal.title.numbered": "محطة طرفية {{number}}", - "terminal.close": "إغلاق المحطة الطرفية", - "terminal.connectionLost.title": "فقد الاتصال", - "terminal.connectionLost.description": "انقطع اتصال المحطة الطرفية. يمكن أن يحدث هذا عند إعادة تشغيل الخادم.", - "common.closeTab": "إغلاق علامة التبويب", - "common.dismiss": "رفض", - "common.requestFailed": "فشل الطلب", - "common.moreOptions": "مزيد من الخيارات", - "common.learnMore": "اعرف المزيد", - "common.rename": "إعادة تسمية", - "common.reset": "إعادة تعيين", - "common.archive": "أرشفة", - "common.delete": "حذف", - "common.close": "إغلاق", - "common.edit": "تحرير", - "common.loadMore": "تحميل المزيد", - "common.key.esc": "ESC", - "sidebar.menu.toggle": "تبديل القائمة", - "sidebar.nav.projectsAndSessions": "المشاريع والجلسات", - "sidebar.settings": "الإعدادات", - "sidebar.help": "مساعدة", - "sidebar.workspaces.enable": "تمكين مساحات العمل", - "sidebar.workspaces.disable": "تعطيل مساحات العمل", - "sidebar.gettingStarted.title": "البدء", - "sidebar.gettingStarted.line1": "يتضمن Kilo نماذج مجانية حتى تتمكن من البدء فورًا.", - "sidebar.gettingStarted.line2": "قم بتوصيل أي موفر لاستخدام النماذج، بما في ذلك Claude و GPT و Gemini وما إلى ذلك.", - "sidebar.project.recentSessions": "الجلسات الحديثة", - "sidebar.project.viewAllSessions": "عرض جميع الجلسات", - "sidebar.project.clearNotifications": "مسح الإشعارات", - "app.name.desktop": "Kilo Desktop", - "settings.section.desktop": "سطح المكتب", - "settings.section.server": "الخادم", - "settings.tab.general": "عام", - "settings.tab.shortcuts": "اختصارات", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "تكامل WSL", - "settings.desktop.wsl.description": "تشغيل خادم Kilo داخل WSL على Windows.", - "settings.general.section.appearance": "المظهر", - "settings.general.section.notifications": "إشعارات النظام", - "settings.general.section.updates": "التحديثات", - "settings.general.section.sounds": "المؤثرات الصوتية", - "settings.general.section.feed": "الخلاصة", - "settings.general.section.display": "شاشة العرض", - "settings.general.row.language.title": "اللغة", - "settings.general.row.language.description": "تغيير لغة العرض لـ Kilo", - "settings.general.row.appearance.title": "المظهر", - "settings.general.row.appearance.description": "تخصيص كيفية ظهور Kilo على جهازك", - "settings.general.row.colorScheme.title": "مخطط الألوان", - "settings.general.row.colorScheme.description": "اختر ما إذا كان Kilo يتبع سمة النظام أو الفاتح أو الداكن", - "settings.general.row.theme.title": "السمة", - "settings.general.row.theme.description": "تخصيص سمة Kilo.", - "settings.general.row.font.title": "خط الكود", - "settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "خط الواجهة", - "settings.general.row.uiFont.description": "خصّص الخط المستخدم في الواجهة بأكملها", - "settings.general.row.followup.title": "سلوك المتابعة", - "settings.general.row.followup.description": "اختر ما إذا كانت طلبات المتابعة توجه فورًا أو تنتظر في قائمة انتظار", - "settings.general.row.followup.option.queue": "قائمة انتظار", - "settings.general.row.followup.option.steer": "توجيه", - "settings.general.row.reasoningSummaries.title": "إظهار ملخصات الاستنتاج", - "settings.general.row.reasoningSummaries.description": "عرض ملخصات استنتاج النموذج في الشريط الزمني", - "settings.general.row.shellToolPartsExpanded.title": "توسيع أجزاء أداة shell", - "settings.general.row.shellToolPartsExpanded.description": - "إظهار أجزاء أداة shell موسعة بشكل افتراضي في الشريط الزمني", - "settings.general.row.editToolPartsExpanded.title": "توسيع أجزاء أداة edit", - "settings.general.row.editToolPartsExpanded.description": - "إظهار أجزاء أدوات edit و write و patch موسعة بشكل افتراضي في الشريط الزمني", - "settings.general.row.showSessionProgressBar.title": "إظهار شريط تقدم الجلسة", - "settings.general.row.showSessionProgressBar.description": "عرض شريط التقدم المتحرك أعلى الجلسة أثناء عمل الوكيل", - "settings.general.row.wayland.title": "استخدام Wayland الأصلي", - "settings.general.row.wayland.description": "تعطيل التراجع إلى X11 على Wayland. يتطلب إعادة التشغيل.", - "settings.general.row.wayland.tooltip": - "على Linux مع شاشات بمعدلات تحديث مختلطة، يمكن أن يكون Wayland الأصلي أكثر استقرارًا.", - "settings.general.row.releaseNotes.title": "ملاحظات الإصدار", - "settings.general.row.releaseNotes.description": 'عرض نوافذ "ما الجديد" المنبثقة بعد التحديثات', - "settings.updates.row.startup.title": "التحقق من التحديثات عند بدء التشغيل", - "settings.updates.row.startup.description": "التحقق تلقائيًا من التحديثات عند تشغيل Kilo", - "settings.updates.row.check.title": "التحقق من التحديثات", - "settings.updates.row.check.description": "التحقق يدويًا من التحديثات وتثبيتها إذا كانت متاحة", - "settings.updates.action.checkNow": "تحقق الآن", - "settings.updates.action.checking": "جارٍ التحقق...", - "settings.updates.toast.latest.title": "أنت على آخر إصدار", - "settings.updates.toast.latest.description": "أنت تستخدم أحدث إصدار من Kilo.", - "sound.option.none": "بلا", - "sound.option.alert01": "تنبيه 01", - "sound.option.alert02": "تنبيه 02", - "sound.option.alert03": "تنبيه 03", - "sound.option.alert04": "تنبيه 04", - "sound.option.alert05": "تنبيه 05", - "sound.option.alert06": "تنبيه 06", - "sound.option.alert07": "تنبيه 07", - "sound.option.alert08": "تنبيه 08", - "sound.option.alert09": "تنبيه 09", - "sound.option.alert10": "تنبيه 10", - "sound.option.bipbop01": "بيب بوب 01", - "sound.option.bipbop02": "بيب بوب 02", - "sound.option.bipbop03": "بيب بوب 03", - "sound.option.bipbop04": "بيب بوب 04", - "sound.option.bipbop05": "بيب بوب 05", - "sound.option.bipbop06": "بيب بوب 06", - "sound.option.bipbop07": "بيب بوب 07", - "sound.option.bipbop08": "بيب بوب 08", - "sound.option.bipbop09": "بيب بوب 09", - "sound.option.bipbop10": "بيب بوب 10", - "sound.option.staplebops01": "ستابل بوبس 01", - "sound.option.staplebops02": "ستابل بوبس 02", - "sound.option.staplebops03": "ستابل بوبس 03", - "sound.option.staplebops04": "ستابل بوبس 04", - "sound.option.staplebops05": "ستابل بوبس 05", - "sound.option.staplebops06": "ستابل بوبس 06", - "sound.option.staplebops07": "ستابل بوبس 07", - "sound.option.nope01": "كلا 01", - "sound.option.nope02": "كلا 02", - "sound.option.nope03": "كلا 03", - "sound.option.nope04": "كلا 04", - "sound.option.nope05": "كلا 05", - "sound.option.nope06": "كلا 06", - "sound.option.nope07": "كلا 07", - "sound.option.nope08": "كلا 08", - "sound.option.nope09": "كلا 09", - "sound.option.nope10": "كلا 10", - "sound.option.nope11": "كلا 11", - "sound.option.nope12": "كلا 12", - "sound.option.yup01": "نعم 01", - "sound.option.yup02": "نعم 02", - "sound.option.yup03": "نعم 03", - "sound.option.yup04": "نعم 04", - "sound.option.yup05": "نعم 05", - "sound.option.yup06": "نعم 06", - "settings.general.notifications.agent.title": "وكيل", - "settings.general.notifications.agent.description": "عرض إشعار النظام عندما يكتمل الوكيل أو يحتاج إلى اهتمام", - "settings.general.notifications.permissions.title": "أذونات", - "settings.general.notifications.permissions.description": "عرض إشعار النظام عند الحاجة إلى إذن", - "settings.general.notifications.errors.title": "أخطاء", - "settings.general.notifications.errors.description": "عرض إشعار النظام عند حدوث خطأ", - "settings.general.sounds.agent.title": "وكيل", - "settings.general.sounds.agent.description": "تشغيل صوت عندما يكتمل الوكيل أو يحتاج إلى اهتمام", - "settings.general.sounds.permissions.title": "أذونات", - "settings.general.sounds.permissions.description": "تشغيل صوت عند الحاجة إلى إذن", - "settings.general.sounds.errors.title": "أخطاء", - "settings.general.sounds.errors.description": "تشغيل صوت عند حدوث خطأ", - "settings.shortcuts.title": "اختصارات لوحة المفاتيح", - "settings.shortcuts.reset.button": "إعادة التعيين إلى الافتراضيات", - "settings.shortcuts.reset.toast.title": "تم إعادة تعيين الاختصارات", - "settings.shortcuts.reset.toast.description": "تم إعادة تعيين اختصارات لوحة المفاتيح إلى الافتراضيات.", - "settings.shortcuts.conflict.title": "الاختصار قيد الاستخدام بالفعل", - "settings.shortcuts.conflict.description": "{{keybind}} معين بالفعل لـ {{titles}}.", - "settings.shortcuts.unassigned": "غير معين", - "settings.shortcuts.pressKeys": "اضغط على المفاتيح", - "settings.shortcuts.search.placeholder": "البحث في الاختصارات", - "settings.shortcuts.search.empty": "لم يتم العثور على اختصارات", - "settings.shortcuts.group.general": "عام", - "settings.shortcuts.group.session": "جلسة", - "settings.shortcuts.group.navigation": "تصفح", - "settings.shortcuts.group.modelAndAgent": "النموذج والوكيل", - "settings.shortcuts.group.terminal": "المحطة الطرفية", - "settings.shortcuts.group.prompt": "موجه", - "settings.providers.title": "الموفرون", - "settings.providers.description": "ستكون إعدادات الموفر قابلة للتكوين هنا.", - "settings.providers.section.connected": "الموفرون المتصلون", - "settings.providers.connected.empty": "لا يوجد موفرون متصلون", - "settings.providers.section.popular": "الموفرون الشائعون", - "settings.providers.tag.environment": "البيئة", - "settings.providers.tag.config": "التكوين", - "settings.providers.tag.custom": "مخصص", - "settings.providers.tag.other": "أخرى", - "settings.models.title": "النماذج", - "settings.models.description": "ستكون إعدادات النموذج قابلة للتكوين هنا.", - "settings.agents.title": "الوكلاء", - "settings.agents.description": "ستكون إعدادات الوكيل قابلة للتكوين هنا.", - "settings.commands.title": "الأوامر", - "settings.commands.description": "ستكون إعدادات الأمر قابلة للتكوين هنا.", - "settings.mcp.title": "MCP", - "settings.mcp.description": "ستكون إعدادات MCP قابلة للتكوين هنا.", - "settings.permissions.title": "الأذونات", - "settings.permissions.description": "تحكم في الأدوات التي يمكن للخادم استخدامها بشكل افتراضي.", - "settings.permissions.section.tools": "الأدوات", - "settings.permissions.toast.updateFailed.title": "فشل تحديث الأذونات", - "settings.permissions.action.allow": "سماح", - "settings.permissions.action.ask": "سؤال", - "settings.permissions.action.deny": "رفض", - "settings.permissions.tool.read.title": "قراءة", - "settings.permissions.tool.read.description": "قراءة ملف (يطابق مسار الملف)", - "settings.permissions.tool.edit.title": "تحرير", - "settings.permissions.tool.edit.description": - "تعديل الملفات، بما في ذلك التحرير والكتابة والتصحيحات والتحرير المتعدد", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "مطابقة الملفات باستخدام أنماط glob", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "البحث في محتويات الملف باستخدام التعبيرات العادية", - "settings.permissions.tool.list.title": "قائمة", - "settings.permissions.tool.list.description": "سرد الملفات داخل دليل", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "تشغيل أوامر shell", - "settings.permissions.tool.task.title": "مهمة", - "settings.permissions.tool.task.description": "تشغيل الوكلاء الفرعيين", - "settings.permissions.tool.skill.title": "مهارة", - "settings.permissions.tool.skill.description": "تحميل مهارة بالاسم", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "تشغيل استعلامات خادم اللغة", - "settings.permissions.tool.todowrite.title": "كتابة المهام", - "settings.permissions.tool.todowrite.description": "تحديث قائمة المهام", - "settings.permissions.tool.webfetch.title": "جلب الويب", - "settings.permissions.tool.webfetch.description": "جلب محتوى من عنوان URL", - "settings.permissions.tool.websearch.title": "بحث الويب", - "settings.permissions.tool.websearch.description": "البحث في الويب", - "settings.permissions.tool.codesearch.title": "بحث الكود", - "settings.permissions.tool.codesearch.description": "البحث عن كود على الويب", - "settings.permissions.tool.external_directory.title": "دليل خارجي", - "settings.permissions.tool.external_directory.description": "الوصول إلى الملفات خارج دليل المشروع", - "settings.permissions.tool.doom_loop.title": "حلقة الموت", - "settings.permissions.tool.doom_loop.description": "اكتشاف استدعاءات الأدوات المتكررة بمدخلات متطابقة", - "session.delete.failed.title": "فشل حذف الجلسة", - "session.delete.title": "حذف الجلسة", - "session.delete.confirm": 'حذف الجلسة "{{name}}"؟', - "session.delete.button": "حذف الجلسة", - "workspace.new": "مساحة عمل جديدة", - "workspace.type.local": "محلي", - "workspace.type.sandbox": "صندوق رمل", - "workspace.create.failed.title": "فشل إنشاء مساحة العمل", - "workspace.delete.failed.title": "فشل حذف مساحة العمل", - "workspace.resetting.title": "إعادة تعيين مساحة العمل", - "workspace.resetting.description": "قد يستغرق هذا دقيقة.", - "workspace.reset.failed.title": "فشل إعادة تعيين مساحة العمل", - "workspace.reset.success.title": "تمت إعادة تعيين مساحة العمل", - "workspace.reset.success.description": "مساحة العمل تطابق الآن الفرع الافتراضي.", - "workspace.error.stillPreparing": "مساحة العمل لا تزال قيد الإعداد", - "workspace.status.checking": "التحقق من التغييرات غير المدمجة...", - "workspace.status.error": "تعذر التحقق من حالة git.", - "workspace.status.clean": "لم يتم اكتشاف تغييرات غير مدمجة.", - "workspace.status.dirty": "تم اكتشاف تغييرات غير مدمجة في مساحة العمل هذه.", - "workspace.delete.title": "حذف مساحة العمل", - "workspace.delete.confirm": 'حذف مساحة العمل "{{name}}"؟', - "workspace.delete.button": "حذف مساحة العمل", - "workspace.reset.title": "إعادة تعيين مساحة العمل", - "workspace.reset.confirm": 'إعادة تعيين مساحة العمل "{{name}}"؟', - "workspace.reset.button": "إعادة تعيين مساحة العمل", - "workspace.reset.archived.none": "لن تتم أرشفة أي جلسات نشطة.", - "workspace.reset.archived.one": "ستتم أرشفة جلسة واحدة.", - "workspace.reset.archived.many": "ستتم أرشفة {{count}} جلسات.", - "workspace.reset.note": "سيؤدي هذا إلى إعادة تعيين مساحة العمل لتتطابق مع الفرع الافتراضي.", - "common.open": "فتح", - "dialog.releaseNotes.action.getStarted": "البدء", - "dialog.releaseNotes.action.next": "التالي", - "dialog.releaseNotes.action.hideFuture": "عدم إظهار هذا في المستقبل", - "dialog.releaseNotes.media.alt": "معاينة الإصدار", - "toast.project.reloadFailed.title": "فشل في إعادة تحميل {{project}}", - "error.server.invalidConfiguration": "تكوين غير صالح", - "common.moreCountSuffix": " (+{{count}} إضافي)", - "common.time.justNow": "الآن", - "common.time.minutesAgo.short": "قبل {{count}} د", - "common.time.hoursAgo.short": "قبل {{count}} س", - "common.time.daysAgo.short": "قبل {{count}} ي", - "settings.providers.connected.environmentDescription": "متصل من متغيرات البيئة الخاصة بك", - "settings.providers.custom.description": "أضف مزود متوافق مع OpenAI بواسطة عنوان URL الأساسي.", - - "app.server.unreachable": "تعذر الوصول إلى {{server}}", - "app.server.retrying": "جاري إعادة المحاولة تلقائيًا...", - "app.server.otherServers": "خوادم أخرى", - "dialog.server.add.usernamePlaceholder": "اسم المستخدم", - "dialog.server.add.passwordPlaceholder": "كلمة المرور", - "server.row.noUsername": "لا يوجد اسم مستخدم", - "session.review.noVcs.createGit.title": "إنشاء مستودع Git", - "session.review.noVcs.createGit.description": "تتبع ومراجعة والتراجع عن التغييرات في هذا المشروع", - "session.review.noVcs.createGit.actionLoading": "جاري إنشاء مستودع Git...", - "session.review.noVcs.createGit.action": "إنشاء مستودع Git", - "session.todo.progress": "تم إكمال {{done}} من {{total}} مهام", - "session.question.progress": "{{current}} من {{total}} أسئلة", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "مستكشف الملفات", - "session.header.open.fileManager": "مدير الملفات", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "المحطة الطرفية", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "تشخيص أداء التطوير", - "debugBar.na": "غير متاح", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": "آخر انتقال مكتمل للمسار يمس صفحة جلسة، مُقاسًا من بدء التوجيه حتى أول رسم بعد استقراره.", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "الإطارات المتجددة في الثانية خلال آخر 5 ثوانٍ.", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "أسوأ وقت للإطار خلال آخر 5 ثوانٍ.", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "الإطارات التي تزيد عن 32 مللي ثانية في آخر 5 ثوانٍ.", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "الوقت المحظور وعدد المهام الطويلة في آخر 5 ثوانٍ. أقصى مهمة: {{max}}.", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "أسوأ تأخير إدخال تمت ملاحظته في آخر 5 ثوانٍ.", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": "مدة التفاعل التقريبية خلال آخر 5 ثوانٍ. هذا يشبه INP، وليس Web Vitals INP الرسمي.", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "التحول التخطيطي التراكمي لعمر التطبيق الحالي.", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "كومة JS المستخدمة مقابل حد الكومة. Chromium فقط.", - "debugBar.mem.tip": "كومة JS المستخدمة مقابل حد الكومة. {{used}} من {{limit}}.", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "Space", - "common.key.backspace": "Backspace", - "common.key.enter": "Enter", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "غير معروف", - "error.page.circular": "[دائري]", - "error.globalSDK.noServerAvailable": "لا يوجد خادم متاح", - "error.globalSDK.serverNotAvailable": "الخادم غير متاح", - "error.childStore.persistedCacheCreateFailed": "فشل إنشاء ذاكرة التخزين المؤقت الدائمة", - "error.childStore.persistedProjectMetadataCreateFailed": "فشل إنشاء بيانات تعريف المشروع الدائمة", - "error.childStore.persistedProjectIconCreateFailed": "فشل إنشاء أيقونة المشروع الدائمة", - "error.childStore.storeCreateFailed": "فشل إنشاء المخزن", - "terminal.connectionLost.abnormalClose": "تم إغلاق WebSocket بشكل غير طبيعي: {{code}}", -} diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts deleted file mode 100644 index 101eb7ce838..00000000000 --- a/packages/app/src/i18n/br.ts +++ /dev/null @@ -1,859 +0,0 @@ -export const dict = { - "command.category.suggested": "Sugerido", - "command.category.view": "Visualizar", - "command.category.project": "Projeto", - "command.category.provider": "Provedor", - "command.category.server": "Servidor", - "command.category.session": "Sessão", - "command.category.theme": "Tema", - "command.category.language": "Idioma", - "command.category.file": "Arquivo", - "command.category.context": "Contexto", - "command.category.terminal": "Terminal", - "command.category.model": "Modelo", - "command.category.mcp": "MCP", - "command.category.agent": "Agente", - "command.category.permissions": "Permissões", - "command.category.workspace": "Espaço de trabalho", - "command.category.settings": "Configurações", - "theme.scheme.system": "Sistema", - "theme.scheme.light": "Claro", - "theme.scheme.dark": "Escuro", - "command.sidebar.toggle": "Alternar barra lateral", - "command.project.open": "Abrir projeto", - "command.provider.connect": "Conectar provedor", - "command.server.switch": "Trocar servidor", - "command.settings.open": "Abrir configurações", - "command.session.previous": "Sessão anterior", - "command.session.next": "Próxima sessão", - "command.session.previous.unseen": "Sessão não lida anterior", - "command.session.next.unseen": "Próxima sessão não lida", - "command.session.archive": "Arquivar sessão", - "command.palette": "Paleta de comandos", - "command.theme.cycle": "Alternar tema", - "command.theme.set": "Usar tema: {{theme}}", - "command.theme.scheme.cycle": "Alternar esquema de cores", - "command.theme.scheme.set": "Usar esquema de cores: {{scheme}}", - "command.language.cycle": "Alternar idioma", - "command.language.set": "Usar idioma: {{language}}", - "command.session.new": "Nova sessão", - "command.file.open": "Abrir arquivo", - "command.tab.close": "Fechar aba", - "command.context.addSelection": "Adicionar seleção ao contexto", - "command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual", - "command.input.focus": "Focar entrada", - "command.terminal.toggle": "Alternar terminal", - "command.fileTree.toggle": "Alternar árvore de arquivos", - "command.review.toggle": "Alternar revisão", - "command.terminal.new": "Novo terminal", - "command.terminal.new.description": "Criar uma nova aba de terminal", - "command.steps.toggle": "Alternar passos", - "command.steps.toggle.description": "Mostrar ou ocultar passos da mensagem atual", - "command.message.previous": "Mensagem anterior", - "command.message.previous.description": "Ir para a mensagem de usuário anterior", - "command.message.next": "Próxima mensagem", - "command.message.next.description": "Ir para a próxima mensagem de usuário", - "command.model.choose": "Escolher modelo", - "command.model.choose.description": "Selecionar um modelo diferente", - "command.mcp.toggle": "Alternar MCPs", - "command.mcp.toggle.description": "Alternar MCPs", - "command.agent.cycle": "Alternar agente", - "command.agent.cycle.description": "Mudar para o próximo agente", - "command.agent.cycle.reverse": "Alternar agente (reverso)", - "command.agent.cycle.reverse.description": "Mudar para o agente anterior", - "command.model.variant.cycle": "Alternar nível de raciocínio", - "command.model.variant.cycle.description": "Mudar para o próximo nível de esforço", - "command.prompt.mode.shell": "Shell", - "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Aceitar permissões automaticamente", - "command.permissions.autoaccept.disable": "Parar de aceitar permissões automaticamente", - "command.workspace.toggle": "Alternar espaços de trabalho", - "command.workspace.toggle.description": "Habilitar ou desabilitar múltiplos espaços de trabalho na barra lateral", - "command.session.undo": "Desfazer", - "command.session.undo.description": "Desfazer a última mensagem", - "command.session.redo": "Refazer", - "command.session.redo.description": "Refazer a última mensagem desfeita", - "command.session.compact": "Compactar sessão", - "command.session.compact.description": "Resumir a sessão para reduzir o tamanho do contexto", - "command.session.fork": "Bifurcar da mensagem", - "command.session.fork.description": "Criar uma nova sessão a partir de uma mensagem anterior", - "command.session.share": "Compartilhar sessão", - "command.session.share.description": "Compartilhar esta sessão e copiar a URL para a área de transferência", - "command.session.unshare": "Parar de compartilhar sessão", - "command.session.unshare.description": "Parar de compartilhar esta sessão", - "palette.search.placeholder": "Buscar arquivos, comandos e sessões", - "palette.empty": "Nenhum resultado encontrado", - "palette.group.commands": "Comandos", - "palette.group.files": "Arquivos", - "dialog.provider.search.placeholder": "Buscar provedores", - "dialog.provider.empty": "Nenhum provedor encontrado", - "dialog.provider.group.popular": "Popular", - "dialog.provider.group.other": "Outro", - "dialog.provider.tag.recommended": "Recomendado", - "dialog.provider.opencode.note": "Modelos selecionados incluindo Claude, GPT, Gemini e mais", - "dialog.provider.opencode.tagline": "Modelos otimizados e confiáveis", - "dialog.provider.opencodeGo.tagline": "Assinatura de baixo custo para todos", - "dialog.provider.anthropic.note": "Conectar com Claude Pro/Max ou chave de API", - "dialog.provider.copilot.note": "Conectar com Copilot ou chave de API", - "dialog.provider.openai.note": "Conectar com ChatGPT Pro/Plus ou chave de API", - "dialog.provider.google.note": "Modelos Gemini para respostas rápidas e estruturadas", - "dialog.provider.openrouter.note": "Acesse todos os modelos suportados de um único provedor", - "dialog.provider.vercel.note": "Acesso unificado a modelos de IA com roteamento inteligente", - "dialog.model.select.title": "Selecionar modelo", - "dialog.model.search.placeholder": "Buscar modelos", - "dialog.model.empty": "Nenhum resultado de modelo", - "dialog.model.manage": "Gerenciar modelos", - "dialog.model.manage.description": "Personalizar quais modelos aparecem no seletor de modelos.", - "dialog.model.manage.provider.toggle": "Alternar todos os modelos {{provider}}", - "dialog.model.unpaid.freeModels.title": "Modelos gratuitos fornecidos pelo Kilo", - "dialog.model.unpaid.addMore.title": "Adicionar mais modelos de provedores populares", - "dialog.provider.viewAll": "Ver mais provedores", - "provider.connect.title": "Conectar {{provider}}", - "provider.connect.title.anthropicProMax": "Entrar com Claude Pro/Max", - "provider.connect.selectMethod": "Selecionar método de login para {{provider}}.", - "provider.connect.method.apiKey": "Chave de API", - "provider.connect.status.inProgress": "Autorização em andamento...", - "provider.connect.status.waiting": "Aguardando autorização...", - "provider.connect.status.failed": "Autorização falhou: {{error}}", - "provider.connect.apiKey.description": - "Digite sua chave de API do {{provider}} para conectar sua conta e usar modelos do {{provider}} no Kilo.", - "provider.connect.apiKey.label": "Chave de API do {{provider}}", - "provider.connect.apiKey.placeholder": "Chave de API", - "provider.connect.apiKey.required": "A chave de API é obrigatória", - "provider.connect.opencodeZen.line1": - "OpenCode Zen oferece acesso a um conjunto selecionado de modelos confiáveis otimizados para agentes de código.", - "provider.connect.opencodeZen.line2": - "Com uma única chave de API você terá acesso a modelos como Claude, GPT, Gemini, GLM e mais.", - "provider.connect.opencodeZen.visit.prefix": "Visite ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " para obter sua chave de API.", - "provider.connect.oauth.code.visit.prefix": "Visite ", - "provider.connect.oauth.code.visit.link": "este link", - "provider.connect.oauth.code.visit.suffix": - " para obter seu código de autorização e conectar sua conta para usar modelos do {{provider}} no Kilo.", - "provider.connect.oauth.code.label": "Código de autorização {{method}}", - "provider.connect.oauth.code.placeholder": "Código de autorização", - "provider.connect.oauth.code.required": "O código de autorização é obrigatório", - "provider.connect.oauth.code.invalid": "Código de autorização inválido", - "provider.connect.oauth.auto.visit.prefix": "Visite ", - "provider.connect.oauth.auto.visit.link": "este link", - "provider.connect.oauth.auto.visit.suffix": - " e digite o código abaixo para conectar sua conta e usar modelos do {{provider}} no Kilo.", - "provider.connect.oauth.auto.confirmationCode": "Código de confirmação", - "provider.connect.toast.connected.title": "{{provider}} conectado", - "provider.connect.toast.connected.description": "Modelos do {{provider}} agora estão disponíveis para uso.", - "provider.custom.title": "Provedor personalizado", - "provider.custom.description.prefix": "Configure um provedor compatível com OpenAI. Veja a ", - "provider.custom.description.link": "documentação de configuração do provedor", - "provider.custom.description.suffix": ".", - "provider.custom.field.providerID.label": "ID do Provedor", - "provider.custom.field.providerID.placeholder": "meuprovedor", - "provider.custom.field.providerID.description": "Letras minúsculas, números, hifens ou sublinhados", - "provider.custom.field.name.label": "Nome de exibição", - "provider.custom.field.name.placeholder": "Meu Provedor de IA", - "provider.custom.field.baseURL.label": "URL Base", - "provider.custom.field.baseURL.placeholder": "https://api.meuprovedor.com/v1", - "provider.custom.field.apiKey.label": "Chave de API", - "provider.custom.field.apiKey.placeholder": "Chave de API", - "provider.custom.field.apiKey.description": "Opcional. Deixe em branco se gerenciar autenticação via cabeçalhos.", - "provider.custom.models.label": "Modelos", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "id-do-modelo", - "provider.custom.models.name.label": "Nome", - "provider.custom.models.name.placeholder": "Nome de Exibição", - "provider.custom.models.remove": "Remover modelo", - "provider.custom.models.add": "Adicionar modelo", - "provider.custom.headers.label": "Cabeçalhos (opcional)", - "provider.custom.headers.key.label": "Cabeçalho", - "provider.custom.headers.key.placeholder": "Nome-Do-Cabeçalho", - "provider.custom.headers.value.label": "Valor", - "provider.custom.headers.value.placeholder": "valor", - "provider.custom.headers.remove": "Remover cabeçalho", - "provider.custom.headers.add": "Adicionar cabeçalho", - "provider.custom.error.providerID.required": "ID do Provedor é obrigatório", - "provider.custom.error.providerID.format": "Use letras minúsculas, números, hifens ou sublinhados", - "provider.custom.error.providerID.exists": "Esse ID de provedor já existe", - "provider.custom.error.name.required": "Nome de exibição é obrigatório", - "provider.custom.error.baseURL.required": "URL Base é obrigatória", - "provider.custom.error.baseURL.format": "Deve começar com http:// ou https://", - "provider.custom.error.required": "Obrigatório", - "provider.custom.error.duplicate": "Duplicado", - "provider.disconnect.toast.disconnected.title": "{{provider}} desconectado", - "provider.disconnect.toast.disconnected.description": "Os modelos de {{provider}} não estão mais disponíveis.", - "model.tag.free": "Grátis", - "model.tag.latest": "Mais recente", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "texto", - "model.input.image": "imagem", - "model.input.audio": "áudio", - "model.input.video": "vídeo", - "model.input.pdf": "pdf", - "model.tooltip.allows": "Permite: {{inputs}}", - "model.tooltip.reasoning.allowed": "Permite raciocínio", - "model.tooltip.reasoning.none": "Sem raciocínio", - "model.tooltip.context": "Limite de contexto {{limit}}", - "common.search.placeholder": "Buscar", - "common.goBack": "Voltar", - "common.goForward": "Avançar", - "common.loading": "Carregando", - "common.loading.ellipsis": "...", - "common.cancel": "Cancelar", - "common.connect": "Conectar", - "common.disconnect": "Desconectar", - "common.continue": "Enviar", - "common.submit": "Enviar", - "common.save": "Salvar", - "common.saving": "Salvando...", - "common.default": "Padrão", - "common.attachment": "anexo", - "prompt.placeholder.shell": "Digite comando do shell... {{example}}", - "prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"', - "prompt.placeholder.simple": "Pergunte qualquer coisa...", - "prompt.placeholder.summarizeComments": "Resumir comentários…", - "prompt.placeholder.summarizeComment": "Resumir comentário…", - "prompt.mode.shell": "Shell", - "prompt.mode.normal": "Prompt", - "prompt.mode.shell.exit": "esc para sair", - "prompt.example.1": "Corrigir um TODO no código", - "prompt.example.2": "Qual é a stack tecnológica deste projeto?", - "prompt.example.3": "Corrigir testes quebrados", - "prompt.example.4": "Explicar como funciona a autenticação", - "prompt.example.5": "Encontrar e corrigir vulnerabilidades de segurança", - "prompt.example.6": "Adicionar testes unitários para o serviço de usuário", - "prompt.example.7": "Refatorar esta função para melhor legibilidade", - "prompt.example.8": "O que significa este erro?", - "prompt.example.9": "Me ajude a depurar este problema", - "prompt.example.10": "Gerar documentação da API", - "prompt.example.11": "Otimizar consultas ao banco de dados", - "prompt.example.12": "Adicionar validação de entrada", - "prompt.example.13": "Criar um novo componente para...", - "prompt.example.14": "Como faço o deploy deste projeto?", - "prompt.example.15": "Revisar meu código para boas práticas", - "prompt.example.16": "Adicionar tratamento de erros a esta função", - "prompt.example.17": "Explicar este padrão regex", - "prompt.example.18": "Converter isto para TypeScript", - "prompt.example.19": "Adicionar logging em todo o código", - "prompt.example.20": "Quais dependências estão desatualizadas?", - "prompt.example.21": "Me ajude a escrever um script de migração", - "prompt.example.22": "Implementar cache para este endpoint", - "prompt.example.23": "Adicionar paginação a esta lista", - "prompt.example.24": "Criar um comando CLI para...", - "prompt.example.25": "Como funcionam as variáveis de ambiente aqui?", - "prompt.popover.emptyResults": "Nenhum resultado correspondente", - "prompt.popover.emptyCommands": "Nenhum comando correspondente", - "prompt.dropzone.label": "Arraste imagens, PDFs ou arquivos de texto aqui", - "prompt.dropzone.file.label": "Solte para @mencionar arquivo", - "prompt.slash.badge.custom": "personalizado", - "prompt.slash.badge.skill": "skill", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "ativo", - "prompt.context.includeActiveFile": "Incluir arquivo ativo", - "prompt.context.removeActiveFile": "Remover arquivo ativo do contexto", - "prompt.context.removeFile": "Remover arquivo do contexto", - "prompt.action.attachFile": "Anexar arquivo", - "prompt.attachment.remove": "Remover anexo", - "prompt.action.send": "Enviar", - "prompt.action.stop": "Parar", - "prompt.toast.pasteUnsupported.title": "Anexo não suportado", - "prompt.toast.pasteUnsupported.description": "Apenas imagens, PDFs ou arquivos de texto podem ser anexados aqui.", - "prompt.toast.modelAgentRequired.title": "Selecione um agente e modelo", - "prompt.toast.modelAgentRequired.description": "Escolha um agente e modelo antes de enviar um prompt.", - "prompt.toast.worktreeCreateFailed.title": "Falha ao criar worktree", - "prompt.toast.sessionCreateFailed.title": "Falha ao criar sessão", - "prompt.toast.shellSendFailed.title": "Falha ao enviar comando shell", - "prompt.toast.commandSendFailed.title": "Falha ao enviar comando", - "prompt.toast.promptSendFailed.title": "Falha ao enviar prompt", - "prompt.toast.promptSendFailed.description": "Não foi possível recuperar a sessão", - "dialog.mcp.title": "MCPs", - "dialog.mcp.description": "{{enabled}} of {{total}} habilitados", - "dialog.mcp.empty": "Nenhum MCP configurado", - "dialog.lsp.empty": "LSPs detectados automaticamente pelos tipos de arquivo", - "dialog.plugins.empty": "Plugins configurados em opencode.json", - "mcp.status.connected": "conectado", - "mcp.status.failed": "falhou", - "mcp.status.needs_auth": "precisa de autenticação", - "mcp.status.disabled": "desabilitado", - "dialog.fork.empty": "Nenhuma mensagem para bifurcar", - "dialog.directory.search.placeholder": "Buscar pastas", - "dialog.directory.empty": "Nenhuma pasta encontrada", - "dialog.server.title": "Servidores", - "dialog.server.description": "Trocar para qual servidor Kilo este aplicativo se conecta.", - "dialog.server.search.placeholder": "Buscar servidores", - "dialog.server.empty": "Nenhum servidor ainda", - "dialog.server.add.title": "Adicionar um servidor", - "dialog.server.add.url": "URL do servidor", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "Não foi possível conectar ao servidor", - "dialog.server.add.checking": "Verificando...", - "dialog.server.add.button": "Adicionar", - "dialog.server.add.name": "Nome do servidor (opcional)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "Nome de usuário (opcional)", - "dialog.server.add.password": "Senha (opcional)", - "dialog.server.edit.title": "Editar servidor", - "dialog.server.default.title": "Servidor padrão", - "dialog.server.default.description": - "Conectar a este servidor na inicialização do aplicativo ao invés de iniciar um servidor local. Requer reinicialização.", - "dialog.server.default.none": "Nenhum servidor selecionado", - "dialog.server.default.set": "Definir servidor atual como padrão", - "dialog.server.default.clear": "Limpar", - "dialog.server.action.remove": "Remover servidor", - "dialog.server.menu.edit": "Editar", - "dialog.server.menu.default": "Definir como padrão", - "dialog.server.menu.defaultRemove": "Remover padrão", - "dialog.server.menu.delete": "Excluir", - "dialog.server.current": "Servidor atual", - "dialog.server.status.default": "Padrão", - "dialog.project.edit.title": "Editar projeto", - "dialog.project.edit.name": "Nome", - "dialog.project.edit.icon": "Ícone", - "dialog.project.edit.icon.alt": "Ícone do projeto", - "dialog.project.edit.icon.hint": "Clique ou arraste uma imagem", - "dialog.project.edit.icon.recommended": "Recomendado: 128x128px", - "dialog.project.edit.color": "Cor", - "dialog.project.edit.color.select": "Selecionar cor {{color}}", - "dialog.project.edit.worktree.startup": "Script de inicialização do espaço de trabalho", - "dialog.project.edit.worktree.startup.description": "Executa após criar um novo espaço de trabalho (worktree).", - "dialog.project.edit.worktree.startup.placeholder": "ex: bun install", - "context.breakdown.title": "Detalhamento do Contexto", - "context.breakdown.note": - 'Detalhamento aproximado dos tokens de entrada. "Outros" inclui definições de ferramentas e overhead.', - "context.breakdown.system": "Sistema", - "context.breakdown.user": "Usuário", - "context.breakdown.assistant": "Assistente", - "context.breakdown.tool": "Chamadas de Ferramentas", - "context.breakdown.other": "Outros", - "context.systemPrompt.title": "Prompt do Sistema", - "context.rawMessages.title": "Mensagens brutas", - "context.stats.session": "Sessão", - "context.stats.messages": "Mensagens", - "context.stats.provider": "Provedor", - "context.stats.model": "Modelo", - "context.stats.limit": "Limite de Contexto", - "context.stats.totalTokens": "Total de Tokens", - "context.stats.usage": "Uso", - "context.stats.inputTokens": "Tokens de Entrada", - "context.stats.outputTokens": "Tokens de Saída", - "context.stats.reasoningTokens": "Tokens de Raciocínio", - "context.stats.cacheTokens": "Tokens de Cache (leitura/escrita)", - "context.stats.userMessages": "Mensagens de Usuário", - "context.stats.assistantMessages": "Mensagens do Assistente", - "context.stats.totalCost": "Custo Total", - "context.stats.sessionCreated": "Sessão Criada", - "context.stats.lastActivity": "Última Atividade", - "context.usage.tokens": "Tokens", - "context.usage.usage": "Uso", - "context.usage.cost": "Custo", - "context.usage.clickToView": "Clique para ver o contexto", - "context.usage.view": "Ver uso do contexto", - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - "toast.language.title": "Idioma", - "toast.language.description": "Alterado para {{language}}", - "toast.theme.title": "Tema alterado", - "toast.scheme.title": "Esquema de cores", - "toast.workspace.enabled.title": "Espaços de trabalho ativados", - "toast.workspace.enabled.description": "Várias worktrees agora são exibidas na barra lateral", - "toast.workspace.disabled.title": "Espaços de trabalho desativados", - "toast.workspace.disabled.description": "Apenas a worktree principal é exibida na barra lateral", - "toast.permissions.autoaccept.on.title": "Aceitando permissões automaticamente", - "toast.permissions.autoaccept.on.description": "Solicitações de permissão serão aprovadas automaticamente", - "toast.permissions.autoaccept.off.title": "Parou de aceitar permissões automaticamente", - "toast.permissions.autoaccept.off.description": "Solicitações de permissão exigirão aprovação", - "toast.model.none.title": "Nenhum modelo selecionado", - "toast.model.none.description": "Conecte um provedor para resumir esta sessão", - "toast.file.loadFailed.title": "Falha ao carregar arquivo", - "toast.file.listFailed.title": "Falha ao listar arquivos", - "toast.context.noLineSelection.title": "Nenhuma seleção de linhas", - "toast.context.noLineSelection.description": "Selecione primeiro um intervalo de linhas em uma aba de arquivo.", - "toast.session.share.copyFailed.title": "Falha ao copiar URL para a área de transferência", - "toast.session.share.success.title": "Sessão compartilhada", - "toast.session.share.success.description": "URL compartilhada copiada para a área de transferência!", - "toast.session.share.failed.title": "Falha ao compartilhar sessão", - "toast.session.share.failed.description": "Ocorreu um erro ao compartilhar a sessão", - "toast.session.unshare.success.title": "Sessão não compartilhada", - "toast.session.unshare.success.description": "Sessão deixou de ser compartilhada com sucesso!", - "toast.session.unshare.failed.title": "Falha ao parar de compartilhar sessão", - "toast.session.unshare.failed.description": "Ocorreu um erro ao parar de compartilhar a sessão", - "toast.session.listFailed.title": "Falha ao carregar sessões para {{project}}", - "toast.update.title": "Atualização disponível", - "toast.update.description": "Uma nova versão do Kilo ({{version}}) está disponível para instalação.", - "toast.update.action.installRestart": "Instalar e reiniciar", - "toast.update.action.notYet": "Agora não", - "error.page.title": "Algo deu errado", - "error.page.description": "Ocorreu um erro ao carregar a aplicação.", - "error.page.details.label": "Detalhes do Erro", - "error.page.action.restart": "Reiniciar", - "error.page.action.checking": "Verificando...", - "error.page.action.checkUpdates": "Verificar atualizações", - "error.page.action.updateTo": "Atualizar para {{version}}", - "error.page.report.prefix": "Por favor, reporte este erro para a equipe do Kilo", - "error.page.report.discord": "no Discord", - "error.page.version": "Versão: {{version}}", - "error.dev.rootNotFound": - "Elemento raiz não encontrado. Você esqueceu de adicioná-lo ao seu index.html? Ou talvez o atributo id foi escrito incorretamente?", - "error.globalSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?", - "directory.error.invalidUrl": "Diretório inválido na URL.", - "error.chain.unknown": "Erro desconhecido", - "error.chain.causedBy": "Causado por:", - "error.chain.apiError": "Erro de API", - "error.chain.status": "Status: {{status}}", - "error.chain.retryable": "Pode tentar novamente: {{retryable}}", - "error.chain.responseBody": "Corpo da resposta:\n{{body}}", - "error.chain.didYouMean": "Você quis dizer: {{suggestions}}", - "error.chain.modelNotFound": "Modelo não encontrado: {{provider}}/{{model}}", - "error.chain.checkConfig": "Verifique os nomes de provedor/modelo na sua configuração (opencode.json)", - "error.chain.mcpFailed": 'Servidor MCP "{{name}}" falhou. Nota: Kilo ainda não suporta autenticação MCP.', - "error.chain.providerAuthFailed": "Autenticação do provedor falhou ({{provider}}): {{message}}", - "error.chain.providerInitFailed": - 'Falha ao inicializar provedor "{{provider}}". Verifique credenciais e configuração.', - "error.chain.configJsonInvalid": "Arquivo de configuração em {{path}} não é um JSON(C) válido", - "error.chain.configJsonInvalidWithMessage": - "Arquivo de configuração em {{path}} não é um JSON(C) válido: {{message}}", - "error.chain.configDirectoryTypo": - 'Diretório "{{dir}}" em {{path}} não é válido. Renomeie o diretório para "{{suggestion}}" ou remova-o. Este é um erro de digitação comum.', - "error.chain.configFrontmatterError": "Falha ao analisar frontmatter em {{path}}:\n{{message}}", - "error.chain.configInvalid": "Arquivo de configuração em {{path}} é inválido", - "error.chain.configInvalidWithMessage": "Arquivo de configuração em {{path}} é inválido: {{message}}", - "notification.permission.title": "Permissão necessária", - "notification.permission.description": "{{sessionTitle}} em {{projectName}} precisa de permissão", - "notification.question.title": "Pergunta", - "notification.question.description": "{{sessionTitle}} em {{projectName}} tem uma pergunta", - "notification.action.goToSession": "Ir para sessão", - "notification.session.responseReady.title": "Resposta pronta", - "notification.session.error.title": "Erro na sessão", - "notification.session.error.fallbackDescription": "Ocorreu um erro", - "home.recentProjects": "Projetos recentes", - "home.empty.title": "Nenhum projeto recente", - "home.empty.description": "Comece abrindo um projeto local", - "session.tab.session": "Sessão", - "session.tab.review": "Revisão", - "session.tab.context": "Contexto", - "session.panel.reviewAndFiles": "Revisão e arquivos", - "session.review.filesChanged": "{{count}} Arquivos Alterados", - "session.review.change.one": "Alteração", - "session.review.change.other": "Alterações", - "session.review.loadingChanges": "Carregando alterações...", - "session.review.empty": "Nenhuma alteração nesta sessão ainda", - "session.review.noVcs": "Nenhum Sistema de Controle de Versão Git detectado, alterações não exibidas", - "session.review.noSnapshot": - "O rastreamento de snapshot está desabilitado na configuração, então as alterações da sessão estão indisponíveis", - "session.review.noChanges": "Sem alterações", - "session.files.selectToOpen": "Selecione um arquivo para abrir", - "session.files.all": "Todos os arquivos", - "session.files.empty": "Nenhum arquivo", - "session.files.binaryContent": "Arquivo binário (conteúdo não pode ser exibido)", - "session.messages.renderEarlier": "Renderizar mensagens anteriores", - "session.messages.loadingEarlier": "Carregando mensagens anteriores...", - "session.messages.loadEarlier": "Carregar mensagens anteriores", - "session.messages.loading": "Carregando mensagens...", - "session.messages.jumpToLatest": "Ir para a mais recente", - "session.context.addToContext": "Adicionar {{selection}} ao contexto", - "session.todo.title": "Tarefas", - "session.todo.collapse": "Recolher", - "session.todo.expand": "Expandir", - "session.followupDock.summary.one": "{{count}} mensagem na fila", - "session.followupDock.summary.other": "{{count}} mensagens na fila", - "session.followupDock.sendNow": "Enviar agora", - "session.followupDock.edit": "Editar", - "session.followupDock.collapse": "Recolher mensagens na fila", - "session.followupDock.expand": "Expandir mensagens na fila", - "session.revertDock.summary.one": "{{count}} mensagem revertida", - "session.revertDock.summary.other": "{{count}} mensagens revertidas", - "session.revertDock.collapse": "Recolher mensagens revertidas", - "session.revertDock.expand": "Expandir mensagens revertidas", - "session.revertDock.restore": "Restaurar mensagem", - "session.new.title": "Crie qualquer coisa", - "session.new.worktree.main": "Branch principal", - "session.new.worktree.mainWithBranch": "Branch principal ({{branch}})", - "session.new.worktree.create": "Criar novo worktree", - "session.new.lastModified": "Última modificação", - "session.header.search.placeholder": "Buscar {{project}}", - "session.header.searchFiles": "Buscar arquivos", - "session.header.openIn": "Abrir em", - "session.header.open.action": "Abrir {{app}}", - "session.header.open.ariaLabel": "Abrir em {{app}}", - "session.header.open.menu": "Opções de abertura", - "session.header.open.copyPath": "Copiar caminho", - "status.popover.trigger": "Status", - "status.popover.ariaLabel": "Configurações de servidores", - "status.popover.tab.servers": "Servidores", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "Plugins", - "status.popover.action.manageServers": "Gerenciar servidores", - "session.share.popover.title": "Publicar na web", - "session.share.popover.description.shared": - "Esta sessão é pública na web. Está acessível para qualquer pessoa com o link.", - "session.share.popover.description.unshared": - "Compartilhar sessão publicamente na web. Estará acessível para qualquer pessoa com o link.", - "session.share.action.share": "Compartilhar", - "session.share.action.publish": "Publicar", - "session.share.action.publishing": "Publicando...", - "session.share.action.unpublish": "Cancelar publicação", - "session.share.action.unpublishing": "Cancelando publicação...", - "session.share.action.view": "Ver", - "session.share.copy.copied": "Copiado", - "session.share.copy.copyLink": "Copiar link", - "lsp.tooltip.none": "Nenhum servidor LSP", - "lsp.label.connected": "{{count}} LSP", - "prompt.loading": "Carregando prompt...", - "terminal.loading": "Carregando terminal...", - "terminal.title": "Terminal", - "terminal.title.numbered": "Terminal {{number}}", - "terminal.close": "Fechar terminal", - "terminal.connectionLost.title": "Conexão Perdida", - "terminal.connectionLost.description": - "A conexão do terminal foi interrompida. Isso pode acontecer quando o servidor reinicia.", - "common.closeTab": "Fechar aba", - "common.dismiss": "Descartar", - "common.requestFailed": "Requisição falhou", - "common.moreOptions": "Mais opções", - "common.learnMore": "Saiba mais", - "common.rename": "Renomear", - "common.reset": "Redefinir", - "common.archive": "Arquivar", - "common.delete": "Excluir", - "common.close": "Fechar", - "common.edit": "Editar", - "common.loadMore": "Carregar mais", - "common.key.esc": "ESC", - "sidebar.menu.toggle": "Alternar menu", - "sidebar.nav.projectsAndSessions": "Projetos e sessões", - "sidebar.settings": "Configurações", - "sidebar.help": "Ajuda", - "sidebar.workspaces.enable": "Habilitar espaços de trabalho", - "sidebar.workspaces.disable": "Desabilitar espaços de trabalho", - "sidebar.gettingStarted.title": "Começando", - "sidebar.gettingStarted.line1": "Kilo inclui modelos gratuitos para você começar imediatamente.", - "sidebar.gettingStarted.line2": "Conecte qualquer provedor para usar modelos, incluindo Claude, GPT, Gemini etc.", - "sidebar.project.recentSessions": "Sessões recentes", - "sidebar.project.viewAllSessions": "Ver todas as sessões", - "sidebar.project.clearNotifications": "Limpar notificações", - "app.name.desktop": "Kilo Desktop", - "settings.section.desktop": "Desktop", - "settings.section.server": "Servidor", - "settings.tab.general": "Geral", - "settings.tab.shortcuts": "Atalhos", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL integration", - "settings.desktop.wsl.description": "Executar o servidor Kilo dentro do WSL no Windows.", - "settings.general.section.appearance": "Aparência", - "settings.general.section.notifications": "Notificações do sistema", - "settings.general.section.updates": "Atualizações", - "settings.general.section.sounds": "Efeitos sonoros", - "settings.general.section.feed": "Feed", - "settings.general.section.display": "Tela", - "settings.general.row.language.title": "Idioma", - "settings.general.row.language.description": "Alterar o idioma de exibição do Kilo", - "settings.general.row.appearance.title": "Aparência", - "settings.general.row.appearance.description": "Personalize como o Kilo aparece no seu dispositivo", - "settings.general.row.colorScheme.title": "Esquema de cores", - "settings.general.row.colorScheme.description": "Escolha se o Kilo segue o tema do sistema, claro ou escuro", - "settings.general.row.theme.title": "Tema", - "settings.general.row.theme.description": "Personalize como o Kilo é tematizado.", - "settings.general.row.font.title": "Fonte de código", - "settings.general.row.font.description": "Personalize a fonte usada em blocos de código", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "Fonte da interface", - "settings.general.row.uiFont.description": "Personalize a fonte usada em toda a interface", - "settings.general.row.followup.title": "Comportamento de acompanhamento", - "settings.general.row.followup.description": - "Escolha se os prompts de acompanhamento orientam imediatamente ou esperam na fila", - "settings.general.row.followup.option.queue": "Fila", - "settings.general.row.followup.option.steer": "Orientar", - "settings.general.row.reasoningSummaries.title": "Mostrar resumos de raciocínio", - "settings.general.row.reasoningSummaries.description": "Exibir resumos de raciocínio do modelo na linha do tempo", - "settings.general.row.shellToolPartsExpanded.title": "Expandir partes da ferramenta shell", - "settings.general.row.shellToolPartsExpanded.description": - "Mostrar partes da ferramenta shell expandidas por padrão na linha do tempo", - "settings.general.row.editToolPartsExpanded.title": "Expandir partes da ferramenta de edição", - "settings.general.row.editToolPartsExpanded.description": - "Mostrar partes das ferramentas de edição, escrita e patch expandidas por padrão na linha do tempo", - "settings.general.row.showSessionProgressBar.title": "Mostrar barra de progresso da sessão", - "settings.general.row.showSessionProgressBar.description": - "Exibir a barra de progresso animada no topo da sessão quando o agente estiver trabalhando", - "settings.general.row.wayland.title": "Usar Wayland nativo", - "settings.general.row.wayland.description": "Desabilitar fallback X11 no Wayland. Requer reinicialização.", - "settings.general.row.wayland.tooltip": - "No Linux com monitores de taxas de atualização mistas, Wayland nativo pode ser mais estável.", - "settings.general.row.releaseNotes.title": "Notas da versão", - "settings.general.row.releaseNotes.description": 'Mostrar pop-ups de "Novidades" após atualizações', - "settings.updates.row.startup.title": "Verificar atualizações ao iniciar", - "settings.updates.row.startup.description": "Verificar atualizações automaticamente quando o Kilo iniciar", - "settings.updates.row.check.title": "Verificar atualizações", - "settings.updates.row.check.description": "Verificar atualizações manualmente e instalar se houver", - "settings.updates.action.checkNow": "Verificar agora", - "settings.updates.action.checking": "Verificando...", - "settings.updates.toast.latest.title": "Você está atualizado", - "settings.updates.toast.latest.description": "Você está usando a versão mais recente do Kilo.", - "sound.option.none": "Nenhum", - "sound.option.alert01": "Alerta 01", - "sound.option.alert02": "Alerta 02", - "sound.option.alert03": "Alerta 03", - "sound.option.alert04": "Alerta 04", - "sound.option.alert05": "Alerta 05", - "sound.option.alert06": "Alerta 06", - "sound.option.alert07": "Alerta 07", - "sound.option.alert08": "Alerta 08", - "sound.option.alert09": "Alerta 09", - "sound.option.alert10": "Alerta 10", - "sound.option.bipbop01": "Bip-bop 01", - "sound.option.bipbop02": "Bip-bop 02", - "sound.option.bipbop03": "Bip-bop 03", - "sound.option.bipbop04": "Bip-bop 04", - "sound.option.bipbop05": "Bip-bop 05", - "sound.option.bipbop06": "Bip-bop 06", - "sound.option.bipbop07": "Bip-bop 07", - "sound.option.bipbop08": "Bip-bop 08", - "sound.option.bipbop09": "Bip-bop 09", - "sound.option.bipbop10": "Bip-bop 10", - "sound.option.staplebops01": "Staplebops 01", - "sound.option.staplebops02": "Staplebops 02", - "sound.option.staplebops03": "Staplebops 03", - "sound.option.staplebops04": "Staplebops 04", - "sound.option.staplebops05": "Staplebops 05", - "sound.option.staplebops06": "Staplebops 06", - "sound.option.staplebops07": "Staplebops 07", - "sound.option.nope01": "Não 01", - "sound.option.nope02": "Não 02", - "sound.option.nope03": "Não 03", - "sound.option.nope04": "Não 04", - "sound.option.nope05": "Não 05", - "sound.option.nope06": "Não 06", - "sound.option.nope07": "Não 07", - "sound.option.nope08": "Não 08", - "sound.option.nope09": "Não 09", - "sound.option.nope10": "Não 10", - "sound.option.nope11": "Não 11", - "sound.option.nope12": "Não 12", - "sound.option.yup01": "Sim 01", - "sound.option.yup02": "Sim 02", - "sound.option.yup03": "Sim 03", - "sound.option.yup04": "Sim 04", - "sound.option.yup05": "Sim 05", - "sound.option.yup06": "Sim 06", - "settings.general.notifications.agent.title": "Agente", - "settings.general.notifications.agent.description": - "Mostrar notificação do sistema quando o agente estiver completo ou precisar de atenção", - "settings.general.notifications.permissions.title": "Permissões", - "settings.general.notifications.permissions.description": - "Mostrar notificação do sistema quando uma permissão for necessária", - "settings.general.notifications.errors.title": "Erros", - "settings.general.notifications.errors.description": "Mostrar notificação do sistema quando ocorrer um erro", - "settings.general.sounds.agent.title": "Agente", - "settings.general.sounds.agent.description": "Reproduzir som quando o agente estiver completo ou precisar de atenção", - "settings.general.sounds.permissions.title": "Permissões", - "settings.general.sounds.permissions.description": "Reproduzir som quando uma permissão for necessária", - "settings.general.sounds.errors.title": "Erros", - "settings.general.sounds.errors.description": "Reproduzir som quando ocorrer um erro", - "settings.shortcuts.title": "Atalhos de teclado", - "settings.shortcuts.reset.button": "Redefinir para padrões", - "settings.shortcuts.reset.toast.title": "Atalhos redefinidos", - "settings.shortcuts.reset.toast.description": "Atalhos de teclado foram redefinidos para os padrões.", - "settings.shortcuts.conflict.title": "Atalho já em uso", - "settings.shortcuts.conflict.description": "{{keybind}} já está atribuído a {{titles}}.", - "settings.shortcuts.unassigned": "Não atribuído", - "settings.shortcuts.pressKeys": "Pressione teclas", - "settings.shortcuts.search.placeholder": "Buscar atalhos", - "settings.shortcuts.search.empty": "Nenhum atalho encontrado", - "settings.shortcuts.group.general": "Geral", - "settings.shortcuts.group.session": "Sessão", - "settings.shortcuts.group.navigation": "Navegação", - "settings.shortcuts.group.modelAndAgent": "Modelo e agente", - "settings.shortcuts.group.terminal": "Terminal", - "settings.shortcuts.group.prompt": "Prompt", - "settings.providers.title": "Provedores", - "settings.providers.description": "Configurações de provedores estarão disponíveis aqui.", - "settings.providers.section.connected": "Provedores conectados", - "settings.providers.connected.empty": "Nenhum provedor conectado", - "settings.providers.section.popular": "Provedores populares", - "settings.providers.tag.environment": "Ambiente", - "settings.providers.tag.config": "Configuração", - "settings.providers.tag.custom": "Personalizado", - "settings.providers.tag.other": "Outro", - "settings.models.title": "Modelos", - "settings.models.description": "Configurações de modelos estarão disponíveis aqui.", - "settings.agents.title": "Agentes", - "settings.agents.description": "Configurações de agentes estarão disponíveis aqui.", - "settings.commands.title": "Comandos", - "settings.commands.description": "Configurações de comandos estarão disponíveis aqui.", - "settings.mcp.title": "MCP", - "settings.mcp.description": "Configurações de MCP estarão disponíveis aqui.", - "settings.permissions.title": "Permissões", - "settings.permissions.description": "Controle quais ferramentas o servidor pode usar por padrão.", - "settings.permissions.section.tools": "Ferramentas", - "settings.permissions.toast.updateFailed.title": "Falha ao atualizar permissões", - "settings.permissions.action.allow": "Permitir", - "settings.permissions.action.ask": "Perguntar", - "settings.permissions.action.deny": "Negar", - "settings.permissions.tool.read.title": "Ler", - "settings.permissions.tool.read.description": "Ler um arquivo (corresponde ao caminho do arquivo)", - "settings.permissions.tool.edit.title": "Editar", - "settings.permissions.tool.edit.description": - "Modificar arquivos, incluindo edições, escritas, patches e multi-edições", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "Corresponder arquivos usando padrões glob", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "Buscar conteúdo de arquivos usando expressões regulares", - "settings.permissions.tool.list.title": "Listar", - "settings.permissions.tool.list.description": "Listar arquivos dentro de um diretório", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "Executar comandos shell", - "settings.permissions.tool.task.title": "Tarefa", - "settings.permissions.tool.task.description": "Lançar sub-agentes", - "settings.permissions.tool.skill.title": "Habilidade", - "settings.permissions.tool.skill.description": "Carregar uma habilidade por nome", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "Executar consultas de servidor de linguagem", - "settings.permissions.tool.todowrite.title": "Escrever Tarefas", - "settings.permissions.tool.todowrite.description": "Atualizar a lista de tarefas", - "settings.permissions.tool.webfetch.title": "Buscar Web", - "settings.permissions.tool.webfetch.description": "Buscar conteúdo de uma URL", - "settings.permissions.tool.websearch.title": "Pesquisa Web", - "settings.permissions.tool.websearch.description": "Pesquisar na web", - "settings.permissions.tool.codesearch.title": "Pesquisa de Código", - "settings.permissions.tool.codesearch.description": "Pesquisar código na web", - "settings.permissions.tool.external_directory.title": "Diretório Externo", - "settings.permissions.tool.external_directory.description": "Acessar arquivos fora do diretório do projeto", - "settings.permissions.tool.doom_loop.title": "Loop Infinito", - "settings.permissions.tool.doom_loop.description": "Detectar chamadas de ferramentas repetidas com entrada idêntica", - "session.delete.failed.title": "Falha ao excluir sessão", - "session.delete.title": "Excluir sessão", - "session.delete.confirm": 'Excluir sessão "{{name}}"?', - "session.delete.button": "Excluir sessão", - "workspace.new": "Novo espaço de trabalho", - "workspace.type.local": "local", - "workspace.type.sandbox": "sandbox", - "workspace.create.failed.title": "Falha ao criar espaço de trabalho", - "workspace.delete.failed.title": "Falha ao excluir espaço de trabalho", - "workspace.resetting.title": "Redefinindo espaço de trabalho", - "workspace.resetting.description": "Isso pode levar um minuto.", - "workspace.reset.failed.title": "Falha ao redefinir espaço de trabalho", - "workspace.reset.success.title": "Espaço de trabalho redefinido", - "workspace.reset.success.description": "Espaço de trabalho agora corresponde ao branch padrão.", - "workspace.error.stillPreparing": "O espaço de trabalho ainda está sendo preparado", - "workspace.status.checking": "Verificando alterações não mescladas...", - "workspace.status.error": "Não foi possível verificar o status do git.", - "workspace.status.clean": "Nenhuma alteração não mesclada detectada.", - "workspace.status.dirty": "Alterações não mescladas detectadas neste espaço de trabalho.", - "workspace.delete.title": "Excluir espaço de trabalho", - "workspace.delete.confirm": 'Excluir espaço de trabalho "{{name}}"?', - "workspace.delete.button": "Excluir espaço de trabalho", - "workspace.reset.title": "Redefinir espaço de trabalho", - "workspace.reset.confirm": 'Redefinir espaço de trabalho "{{name}}"?', - "workspace.reset.button": "Redefinir espaço de trabalho", - "workspace.reset.archived.none": "Nenhuma sessão ativa será arquivada.", - "workspace.reset.archived.one": "1 sessão será arquivada.", - "workspace.reset.archived.many": "{{count}} sessões serão arquivadas.", - "workspace.reset.note": "Isso redefinirá o espaço de trabalho para corresponder ao branch padrão.", - "common.open": "Abrir", - "dialog.releaseNotes.action.getStarted": "Começar", - "dialog.releaseNotes.action.next": "Próximo", - "dialog.releaseNotes.action.hideFuture": "Não mostrar isso no futuro", - "dialog.releaseNotes.media.alt": "Prévia do lançamento", - "toast.project.reloadFailed.title": "Falha ao recarregar {{project}}", - "error.server.invalidConfiguration": "Configuração inválida", - "common.moreCountSuffix": " (+{{count}} mais)", - "common.time.justNow": "Agora mesmo", - "common.time.minutesAgo.short": "{{count}}m atrás", - "common.time.hoursAgo.short": "{{count}}h atrás", - "common.time.daysAgo.short": "{{count}}d atrás", - "settings.providers.connected.environmentDescription": "Conectado a partir de suas variáveis de ambiente", - "settings.providers.custom.description": "Adicionar um provedor compatível com a OpenAI através do URL base.", - - "app.server.unreachable": "Não foi possível conectar a {{server}}", - "app.server.retrying": "Tentando novamente automaticamente...", - "app.server.otherServers": "Outros servidores", - "dialog.server.add.usernamePlaceholder": "nome de usuário", - "dialog.server.add.passwordPlaceholder": "senha", - "server.row.noUsername": "sem nome de usuário", - "session.review.noVcs.createGit.title": "Criar um repositório Git", - "session.review.noVcs.createGit.description": "Rastreie, revise e desfaça alterações neste projeto", - "session.review.noVcs.createGit.actionLoading": "Criando repositório Git...", - "session.review.noVcs.createGit.action": "Criar repositório Git", - "session.todo.progress": "{{done}} de {{total}} tarefas concluídas", - "session.question.progress": "{{current}} de {{total}} perguntas", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "Explorador de Arquivos", - "session.header.open.fileManager": "Gerenciador de Arquivos", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "Terminal", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "Diagnóstico de desempenho de desenvolvimento", - "debugBar.na": "n/a", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": - "Última transição de rota concluída tocando em uma página de sessão, medida desde o início do roteador até a primeira pintura após o estabelecimento.", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "Quadros por segundo nos últimos 5 segundos.", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "Pior tempo de quadro nos últimos 5 segundos.", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "Quadros acima de 32ms nos últimos 5 segundos.", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "Tempo bloqueado e contagem de tarefas longas nos últimos 5 segundos. Tarefa máx: {{max}}.", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "Pior atraso de entrada observado nos últimos 5 segundos.", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": - "Duração aproximada da interação nos últimos 5 segundos. Isso é semelhante ao INP, não o INP oficial do Web Vitals.", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "Mudança cumulativa de layout para o tempo de vida atual do aplicativo.", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "Heap JS usado vs limite de heap. Apenas Chromium.", - "debugBar.mem.tip": "Heap JS usado vs limite de heap. {{used}} de {{limit}}.", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "Espaço", - "common.key.backspace": "Backspace", - "common.key.enter": "Enter", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "desconhecido", - "error.page.circular": "[Circular]", - "error.globalSDK.noServerAvailable": "Nenhum servidor disponível", - "error.globalSDK.serverNotAvailable": "Servidor indisponível", - "error.childStore.persistedCacheCreateFailed": "Falha ao criar cache persistente", - "error.childStore.persistedProjectMetadataCreateFailed": "Falha ao criar metadados de projeto persistentes", - "error.childStore.persistedProjectIconCreateFailed": "Falha ao criar ícone de projeto persistente", - "error.childStore.storeCreateFailed": "Falha ao criar armazenamento", - "terminal.connectionLost.abnormalClose": "WebSocket fechado anormalmente: {{code}}", -} diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts deleted file mode 100644 index a52b04c437f..00000000000 --- a/packages/app/src/i18n/bs.ts +++ /dev/null @@ -1,935 +0,0 @@ -export const dict = { - "command.category.suggested": "Predloženo", - "command.category.view": "Prikaz", - "command.category.project": "Projekat", - "command.category.provider": "Provajder", - "command.category.server": "Server", - "command.category.session": "Sesija", - "command.category.theme": "Tema", - "command.category.language": "Jezik", - "command.category.file": "Datoteka", - "command.category.context": "Kontekst", - "command.category.terminal": "Terminal", - "command.category.model": "Model", - "command.category.mcp": "MCP", - "command.category.agent": "Agent", - "command.category.permissions": "Dozvole", - "command.category.workspace": "Radni prostor", - "command.category.settings": "Postavke", - - "theme.scheme.system": "Sistem", - "theme.scheme.light": "Svijetlo", - "theme.scheme.dark": "Tamno", - - "command.sidebar.toggle": "Prikaži/sakrij bočnu traku", - "command.project.open": "Otvori projekat", - "command.provider.connect": "Poveži provajdera", - "command.server.switch": "Promijeni server", - "command.settings.open": "Otvori postavke", - "command.session.previous": "Prethodna sesija", - "command.session.next": "Sljedeća sesija", - "command.session.previous.unseen": "Prethodna nepročitana sesija", - "command.session.next.unseen": "Sljedeća nepročitana sesija", - "command.session.archive": "Arhiviraj sesiju", - - "command.palette": "Paleta komandi", - - "command.theme.cycle": "Promijeni temu", - "command.theme.set": "Koristi temu: {{theme}}", - "command.theme.scheme.cycle": "Promijeni šemu boja", - "command.theme.scheme.set": "Koristi šemu boja: {{scheme}}", - - "command.language.cycle": "Promijeni jezik", - "command.language.set": "Koristi jezik: {{language}}", - - "command.session.new": "Nova sesija", - "command.file.open": "Otvori datoteku", - "command.tab.close": "Zatvori karticu", - "command.context.addSelection": "Dodaj odabir u kontekst", - "command.context.addSelection.description": "Dodaj odabrane linije iz trenutne datoteke", - "command.input.focus": "Fokusiraj polje za unos", - "command.terminal.toggle": "Prikaži/sakrij terminal", - "command.fileTree.toggle": "Prikaži/sakrij stablo datoteka", - "command.review.toggle": "Prikaži/sakrij pregled", - "command.terminal.new": "Novi terminal", - "command.terminal.new.description": "Kreiraj novu karticu terminala", - "command.steps.toggle": "Prikaži/sakrij korake", - "command.steps.toggle.description": "Prikaži ili sakrij korake za trenutnu poruku", - "command.message.previous": "Prethodna poruka", - "command.message.previous.description": "Idi na prethodnu korisničku poruku", - "command.message.next": "Sljedeća poruka", - "command.message.next.description": "Idi na sljedeću korisničku poruku", - "command.model.choose": "Odaberi model", - "command.model.choose.description": "Odaberi drugi model", - "command.mcp.toggle": "Prikaži/sakrij MCP-ove", - "command.mcp.toggle.description": "Prikaži/sakrij MCP-ove", - "command.agent.cycle": "Promijeni agenta", - "command.agent.cycle.description": "Prebaci na sljedećeg agenta", - "command.agent.cycle.reverse": "Promijeni agenta unazad", - "command.agent.cycle.reverse.description": "Prebaci na prethodnog agenta", - "command.model.variant.cycle": "Promijeni nivo razmišljanja", - "command.model.variant.cycle.description": "Prebaci na sljedeći nivo", - "command.prompt.mode.shell": "Shell", - "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Automatski prihvati dozvole", - "command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje dozvola", - "command.workspace.toggle": "Prikaži/sakrij radne prostore", - "command.workspace.toggle.description": "Omogući ili onemogući više radnih prostora u bočnoj traci", - "command.session.undo": "Poništi", - "command.session.undo.description": "Poništi posljednju poruku", - "command.session.redo": "Vrati", - "command.session.redo.description": "Vrati posljednju poništenu poruku", - "command.session.compact": "Sažmi sesiju", - "command.session.compact.description": "Sažmi sesiju kako bi se smanjio kontekst", - "command.session.fork": "Fork iz poruke", - "command.session.fork.description": "Kreiraj novu sesiju iz prethodne poruke", - "command.session.share": "Podijeli sesiju", - "command.session.share.description": "Podijeli ovu sesiju i kopiraj URL u međuspremnik", - "command.session.unshare": "Ukini dijeljenje sesije", - "command.session.unshare.description": "Zaustavi dijeljenje ove sesije", - - "palette.search.placeholder": "Pretraži datoteke, komande i sesije", - "palette.empty": "Nema rezultata", - "palette.group.commands": "Komande", - "palette.group.files": "Datoteke", - - "dialog.provider.search.placeholder": "Pretraži provajdere", - "dialog.provider.empty": "Nema pronađenih provajdera", - "dialog.provider.group.popular": "Popularno", - "dialog.provider.group.other": "Ostalo", - "dialog.provider.tag.recommended": "Preporučeno", - "dialog.provider.opencode.note": "Kurirani modeli uključujući Claude, GPT, Gemini i druge", - "dialog.provider.opencode.tagline": "Pouzdani optimizovani modeli", - "dialog.provider.opencodeGo.tagline": "Povoljna pretplata za sve", - "dialog.provider.anthropic.note": "Direktan pristup Claude modelima, uključujući Pro i Max", - "dialog.provider.copilot.note": "AI modeli za pomoć pri kodiranju putem GitHub Copilot", - "dialog.provider.openai.note": "GPT modeli za brze, sposobne opšte AI zadatke", - "dialog.provider.google.note": "Gemini modeli za brze, strukturirane odgovore", - "dialog.provider.openrouter.note": "Pristup svim podržanim modelima preko jednog provajdera", - "dialog.provider.vercel.note": "Jedinstven pristup AI modelima uz pametno rutiranje", - - "dialog.model.select.title": "Odaberi model", - "dialog.model.search.placeholder": "Pretraži modele", - "dialog.model.empty": "Nema rezultata za modele", - "dialog.model.manage": "Upravljaj modelima", - "dialog.model.manage.description": "Prilagodi koji se modeli prikazuju u izborniku modela.", - "dialog.model.manage.provider.toggle": "Uključi/isključi sve {{provider}} modele", - - "dialog.model.unpaid.freeModels.title": "Besplatni modeli koje obezbjeđuje Kilo", - "dialog.model.unpaid.addMore.title": "Dodaj još modela od popularnih provajdera", - - "dialog.provider.viewAll": "Prikaži više provajdera", - - "provider.connect.title": "Poveži {{provider}}", - "provider.connect.title.anthropicProMax": "Prijavi se putem Claude Pro/Max", - "provider.connect.selectMethod": "Odaberi način prijave za {{provider}}.", - "provider.connect.method.apiKey": "API ključ", - "provider.connect.status.inProgress": "Autorizacija je u toku...", - "provider.connect.status.waiting": "Čekanje na autorizaciju...", - "provider.connect.status.failed": "Autorizacija nije uspjela: {{error}}", - "provider.connect.apiKey.description": - "Unesi svoj {{provider}} API ključ da povežeš račun i koristiš {{provider}} modele u Kilo-u.", - "provider.connect.apiKey.label": "{{provider}} API ključ", - "provider.connect.apiKey.placeholder": "API ključ", - "provider.connect.apiKey.required": "API ključ je obavezan", - "provider.connect.opencodeZen.line1": - "OpenCode Zen ti daje pristup kuriranom skupu pouzdanih, optimizovanih modela za coding agente.", - "provider.connect.opencodeZen.line2": - "Sa jednim API ključem dobijaš pristup modelima kao što su Claude, GPT, Gemini, GLM i drugi.", - "provider.connect.opencodeZen.visit.prefix": "Posjeti ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " da preuzmeš svoj API ključ.", - "provider.connect.oauth.code.visit.prefix": "Posjeti ", - "provider.connect.oauth.code.visit.link": "ovaj link", - "provider.connect.oauth.code.visit.suffix": - " da preuzmeš autorizacijski kod i povežeš račun te koristiš {{provider}} modele u Kilo-u.", - "provider.connect.oauth.code.label": "{{method}} autorizacijski kod", - "provider.connect.oauth.code.placeholder": "Autorizacijski kod", - "provider.connect.oauth.code.required": "Autorizacijski kod je obavezan", - "provider.connect.oauth.code.invalid": "Nevažeći autorizacijski kod", - "provider.connect.oauth.auto.visit.prefix": "Posjeti ", - "provider.connect.oauth.auto.visit.link": "ovaj link", - "provider.connect.oauth.auto.visit.suffix": - " i unesi kod ispod da povežeš račun i koristiš {{provider}} modele u Kilo-u.", - "provider.connect.oauth.auto.confirmationCode": "Kod za potvrdu", - "provider.connect.toast.connected.title": "{{provider}} povezan", - "provider.connect.toast.connected.description": "{{provider}} modeli su sada dostupni za korištenje.", - - "provider.custom.title": "Prilagođeni provajder", - "provider.custom.description.prefix": "Konfiguriši OpenAI-kompatibilnog provajdera. Pogledaj ", - "provider.custom.description.link": "dokumentaciju za konfiguraciju provajdera", - "provider.custom.description.suffix": ".", - "provider.custom.field.providerID.label": "ID provajdera", - "provider.custom.field.providerID.placeholder": "mojprovajder", - "provider.custom.field.providerID.description": "Mala slova, brojevi, crtice ili donje crte", - "provider.custom.field.name.label": "Prikazano ime", - "provider.custom.field.name.placeholder": "Moj AI Provajder", - "provider.custom.field.baseURL.label": "Bazni URL", - "provider.custom.field.baseURL.placeholder": "https://api.mojprovajder.com/v1", - "provider.custom.field.apiKey.label": "API ključ", - "provider.custom.field.apiKey.placeholder": "API ključ", - "provider.custom.field.apiKey.description": - "Opcionalno. Ostavi prazno ako upravljaš autentifikacijom putem zaglavlja.", - "provider.custom.models.label": "Modeli", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "model-id", - "provider.custom.models.name.label": "Ime", - "provider.custom.models.name.placeholder": "Prikazano ime", - "provider.custom.models.remove": "Ukloni model", - "provider.custom.models.add": "Dodaj model", - "provider.custom.headers.label": "Zaglavlja (opcionalno)", - "provider.custom.headers.key.label": "Zaglavlje", - "provider.custom.headers.key.placeholder": "Ime-Zaglavlja", - "provider.custom.headers.value.label": "Vrijednost", - "provider.custom.headers.value.placeholder": "vrijednost", - "provider.custom.headers.remove": "Ukloni zaglavlje", - "provider.custom.headers.add": "Dodaj zaglavlje", - "provider.custom.error.providerID.required": "ID provajdera je obavezan", - "provider.custom.error.providerID.format": "Koristi mala slova, brojeve, crtice ili donje crte", - "provider.custom.error.providerID.exists": "Taj ID provajdera već postoji", - "provider.custom.error.name.required": "Prikazano ime je obavezno", - "provider.custom.error.baseURL.required": "Bazni URL je obavezan", - "provider.custom.error.baseURL.format": "Mora početi sa http:// ili https://", - "provider.custom.error.required": "Obavezno", - "provider.custom.error.duplicate": "Duplikat", - - "provider.disconnect.toast.disconnected.title": "{{provider}} odspojen", - "provider.disconnect.toast.disconnected.description": "{{provider}} modeli više nisu dostupni.", - - "model.tag.free": "Besplatno", - "model.tag.latest": "Najnovije", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "tekst", - "model.input.image": "slika", - "model.input.audio": "zvuk", - "model.input.video": "video", - "model.input.pdf": "pdf", - "model.tooltip.allows": "Podržava: {{inputs}}", - "model.tooltip.reasoning.allowed": "Podržava rasuđivanje", - "model.tooltip.reasoning.none": "Bez rasuđivanja", - "model.tooltip.context": "Limit konteksta {{limit}}", - - "common.search.placeholder": "Pretraži", - "common.goBack": "Nazad", - "common.goForward": "Naprijed", - "common.loading": "Učitavanje", - "common.loading.ellipsis": "...", - "common.cancel": "Otkaži", - "common.connect": "Poveži", - "common.disconnect": "Prekini vezu", - "common.continue": "Pošalji", - "common.submit": "Pošalji", - "common.save": "Sačuvaj", - "common.saving": "Čuvanje...", - "common.default": "Podrazumijevano", - "common.attachment": "prilog", - - "prompt.placeholder.shell": "Unesi shell naredbu... {{example}}", - "prompt.placeholder.normal": 'Pitaj bilo šta... "{{example}}"', - "prompt.placeholder.simple": "Pitaj bilo šta...", - "prompt.placeholder.summarizeComments": "Sažmi komentare…", - "prompt.placeholder.summarizeComment": "Sažmi komentar…", - "prompt.mode.shell": "Shell", - "prompt.mode.normal": "Prompt", - "prompt.mode.shell.exit": "esc za izlaz", - - "prompt.example.1": "Popravi TODO u bazi koda", - "prompt.example.2": "Koji je tehnološki stack ovog projekta?", - "prompt.example.3": "Popravi pokvarene testove", - "prompt.example.4": "Objasni kako radi autentifikacija", - "prompt.example.5": "Pronađi i popravi sigurnosne ranjivosti", - "prompt.example.6": "Dodaj jedinične testove za servis korisnika", - "prompt.example.7": "Refaktoriši ovu funkciju da bude čitljivija", - "prompt.example.8": "Šta znači ova greška?", - "prompt.example.9": "Pomozi mi da otklonim ovu grešku", - "prompt.example.10": "Generiši API dokumentaciju", - "prompt.example.11": "Optimizuj upite prema bazi podataka", - "prompt.example.12": "Dodaj validaciju ulaza", - "prompt.example.13": "Napravi novu komponentu za...", - "prompt.example.14": "Kako da deployam ovaj projekat?", - "prompt.example.15": "Pregledaj moj kod prema najboljim praksama", - "prompt.example.16": "Dodaj obradu grešaka u ovu funkciju", - "prompt.example.17": "Objasni ovaj regex obrazac", - "prompt.example.18": "Pretvori ovo u TypeScript", - "prompt.example.19": "Dodaj logovanje kroz cijelu bazu koda", - "prompt.example.20": "Koje su zavisnosti zastarjele?", - "prompt.example.21": "Pomozi mi da napišem migracijsku skriptu", - "prompt.example.22": "Implementiraj keširanje za ovaj endpoint", - "prompt.example.23": "Dodaj paginaciju u ovu listu", - "prompt.example.24": "Napravi CLI komandu za...", - "prompt.example.25": "Kako ovdje rade varijable okruženja?", - - "prompt.popover.emptyResults": "Nema rezultata", - "prompt.popover.emptyCommands": "Nema komandi", - "prompt.dropzone.label": "Ovdje prevucite slike, PDF-ove ili tekstualne datoteke", - "prompt.dropzone.file.label": "Spusti za @spominjanje datoteke", - "prompt.slash.badge.custom": "prilagođeno", - "prompt.slash.badge.skill": "skill", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "aktivno", - "prompt.context.includeActiveFile": "Uključi aktivnu datoteku", - "prompt.context.removeActiveFile": "Ukloni aktivnu datoteku iz konteksta", - "prompt.context.removeFile": "Ukloni datoteku iz konteksta", - "prompt.action.attachFile": "Priloži datoteku", - "prompt.attachment.remove": "Ukloni prilog", - "prompt.action.send": "Pošalji", - "prompt.action.stop": "Zaustavi", - - "prompt.toast.pasteUnsupported.title": "Nepodržan prilog", - "prompt.toast.pasteUnsupported.description": "Ovdje se mogu priložiti samo slike, PDF-ovi ili tekstualne datoteke.", - "prompt.toast.modelAgentRequired.title": "Odaberi agenta i model", - "prompt.toast.modelAgentRequired.description": "Odaberi agenta i model prije slanja upita.", - "prompt.toast.worktreeCreateFailed.title": "Neuspješno kreiranje worktree-a", - "prompt.toast.sessionCreateFailed.title": "Neuspješno kreiranje sesije", - "prompt.toast.shellSendFailed.title": "Neuspješno slanje shell naredbe", - "prompt.toast.commandSendFailed.title": "Neuspješno slanje komande", - "prompt.toast.promptSendFailed.title": "Neuspješno slanje upita", - "prompt.toast.promptSendFailed.description": "Nije moguće dohvatiti sesiju", - - "dialog.mcp.title": "MCP-ovi", - "dialog.mcp.description": "{{enabled}} od {{total}} omogućeno", - "dialog.mcp.empty": "Nema konfigurisnih MCP-ova", - - "dialog.lsp.empty": "LSP-ovi se automatski otkrivaju prema tipu datoteke", - "dialog.plugins.empty": "Plugini su konfigurisani u opencode.json", - - "mcp.status.connected": "povezano", - "mcp.status.failed": "neuspjelo", - "mcp.status.needs_auth": "potrebna autentifikacija", - "mcp.status.disabled": "onemogućeno", - - "dialog.fork.empty": "Nema poruka za fork", - - "dialog.directory.search.placeholder": "Pretraži foldere", - "dialog.directory.empty": "Nema pronađenih foldera", - - "dialog.server.title": "Serveri", - "dialog.server.description": "Promijeni na koji se Kilo server ova aplikacija povezuje.", - "dialog.server.search.placeholder": "Pretraži servere", - "dialog.server.empty": "Još nema servera", - "dialog.server.add.title": "Dodaj server", - "dialog.server.add.url": "URL servera", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "Nije moguće povezati se na server", - "dialog.server.add.checking": "Provjera...", - "dialog.server.add.button": "Dodaj server", - "dialog.server.add.name": "Ime servera (opcionalno)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "Korisničko ime (opcionalno)", - "dialog.server.add.password": "Lozinka (opcionalno)", - "dialog.server.edit.title": "Uredi server", - "dialog.server.default.title": "Podrazumijevani server", - "dialog.server.default.description": - "Poveži se na ovaj server pri pokretanju aplikacije umjesto pokretanja lokalnog servera. Potreban je restart.", - "dialog.server.default.none": "Nije odabran server", - "dialog.server.default.set": "Postavi trenutni server kao podrazumijevani", - "dialog.server.default.clear": "Očisti", - "dialog.server.action.remove": "Ukloni server", - - "dialog.server.menu.edit": "Uredi", - "dialog.server.menu.default": "Postavi kao podrazumijevano", - "dialog.server.menu.defaultRemove": "Ukloni podrazumijevano", - "dialog.server.menu.delete": "Izbriši", - "dialog.server.current": "Trenutni server", - "dialog.server.status.default": "Podrazumijevano", - - "dialog.project.edit.title": "Uredi projekat", - "dialog.project.edit.name": "Naziv", - "dialog.project.edit.icon": "Ikonica", - "dialog.project.edit.icon.alt": "Ikonica projekta", - "dialog.project.edit.icon.hint": "Klikni ili prevuci sliku", - "dialog.project.edit.icon.recommended": "Preporučeno: 128x128px", - "dialog.project.edit.color": "Boja", - "dialog.project.edit.color.select": "Odaberi boju {{color}}", - "dialog.project.edit.worktree.startup": "Skripta za pokretanje radnog prostora", - "dialog.project.edit.worktree.startup.description": "Pokreće se nakon kreiranja novog radnog prostora (worktree).", - "dialog.project.edit.worktree.startup.placeholder": "npr. bun install", - - "context.breakdown.title": "Razlaganje konteksta", - "context.breakdown.note": - 'Približna raspodjela ulaznih tokena. "Ostalo" uključuje definicije alata i dodatni overhead.', - "context.breakdown.system": "Sistem", - "context.breakdown.user": "Korisnik", - "context.breakdown.assistant": "Asistent", - "context.breakdown.tool": "Pozivi alata", - "context.breakdown.other": "Ostalo", - - "context.systemPrompt.title": "Sistemski prompt", - "context.rawMessages.title": "Sirove poruke", - - "context.stats.session": "Sesija", - "context.stats.messages": "Poruke", - "context.stats.provider": "Provajder", - "context.stats.model": "Model", - "context.stats.limit": "Limit konteksta", - "context.stats.totalTokens": "Ukupno tokena", - "context.stats.usage": "Korištenje", - "context.stats.inputTokens": "Ulazni tokeni", - "context.stats.outputTokens": "Izlazni tokeni", - "context.stats.reasoningTokens": "Tokeni za rasuđivanje", - "context.stats.cacheTokens": "Cache tokeni (čitanje/pisanje)", - "context.stats.userMessages": "Korisničke poruke", - "context.stats.assistantMessages": "Poruke asistenta", - "context.stats.totalCost": "Ukupni trošak", - "context.stats.sessionCreated": "Sesija kreirana", - "context.stats.lastActivity": "Posljednja aktivnost", - - "context.usage.tokens": "Tokeni", - "context.usage.usage": "Korištenje", - "context.usage.cost": "Trošak", - "context.usage.clickToView": "Klikni da vidiš kontekst", - "context.usage.view": "Prikaži korištenje konteksta", - - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - - "toast.language.title": "Jezik", - "toast.language.description": "Prebačeno na {{language}}", - - "toast.theme.title": "Tema promijenjena", - "toast.scheme.title": "Šema boja", - - "toast.workspace.enabled.title": "Radni prostori omogućeni", - "toast.workspace.enabled.description": "Više worktree-ova se sada prikazuje u bočnoj traci", - "toast.workspace.disabled.title": "Radni prostori onemogućeni", - "toast.workspace.disabled.description": "Samo glavni worktree se prikazuje u bočnoj traci", - - "toast.permissions.autoaccept.on.title": "Automatsko prihvatanje dozvola", - "toast.permissions.autoaccept.on.description": "Zahtjevi za dozvole će biti automatski odobreni", - "toast.permissions.autoaccept.off.title": "Zaustavljeno automatsko prihvatanje dozvola", - "toast.permissions.autoaccept.off.description": "Zahtjevi za dozvole će zahtijevati odobrenje", - - "toast.model.none.title": "Nije odabran model", - "toast.model.none.description": "Poveži provajdera da sažmeš ovu sesiju", - - "toast.file.loadFailed.title": "Neuspjelo učitavanje datoteke", - "toast.file.listFailed.title": "Neuspješno listanje datoteka", - - "toast.context.noLineSelection.title": "Nema odabranih linija", - "toast.context.noLineSelection.description": "Prvo odaberi raspon linija u kartici datoteke.", - - "toast.session.share.copyFailed.title": "Neuspjelo kopiranje URL-a u međuspremnik", - "toast.session.share.success.title": "Sesija podijeljena", - "toast.session.share.success.description": "URL za dijeljenje je kopiran u međuspremnik!", - "toast.session.share.failed.title": "Neuspjelo dijeljenje sesije", - "toast.session.share.failed.description": "Došlo je do greške prilikom dijeljenja sesije", - - "toast.session.unshare.success.title": "Dijeljenje sesije ukinuto", - "toast.session.unshare.success.description": "Dijeljenje sesije je uspješno ukinuto!", - "toast.session.unshare.failed.title": "Neuspjelo ukidanje dijeljenja", - "toast.session.unshare.failed.description": "Došlo je do greške prilikom ukidanja dijeljenja", - - "toast.session.listFailed.title": "Neuspjelo učitavanje sesija za {{project}}", - - "toast.update.title": "Dostupno ažuriranje", - "toast.update.description": "Nova verzija Kilo-a ({{version}}) je dostupna za instalaciju.", - "toast.update.action.installRestart": "Instaliraj i restartuj", - "toast.update.action.notYet": "Ne još", - - "error.page.title": "Nešto je pošlo po zlu", - "error.page.description": "Došlo je do greške prilikom učitavanja aplikacije.", - "error.page.details.label": "Detalji greške", - "error.page.action.restart": "Restartuj", - "error.page.action.checking": "Provjera...", - "error.page.action.checkUpdates": "Provjeri ažuriranja", - "error.page.action.updateTo": "Ažuriraj na {{version}}", - "error.page.report.prefix": "Molimo prijavi ovu grešku Kilo timu", - "error.page.report.discord": "na Discordu", - "error.page.version": "Verzija: {{version}}", - - "error.dev.rootNotFound": - "Korijenski element nije pronađen. Da li si zaboravio da ga dodaš u index.html? Ili je možda id atribut pogrešno napisan?", - - "error.globalSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?", - "directory.error.invalidUrl": "Nevažeći direktorij u URL-u.", - - "error.chain.unknown": "Nepoznata greška", - "error.chain.causedBy": "Uzrok:", - "error.chain.apiError": "API greška", - "error.chain.status": "Status: {{status}}", - "error.chain.retryable": "Može se ponoviti: {{retryable}}", - "error.chain.responseBody": "Tijelo odgovora:\n{{body}}", - "error.chain.didYouMean": "Da li si mislio: {{suggestions}}", - "error.chain.modelNotFound": "Model nije pronađen: {{provider}}/{{model}}", - "error.chain.checkConfig": "Provjeri konfiguraciju (opencode.json) provider/model names", - "error.chain.mcpFailed": 'MCP server "{{name}}" nije uspio. Napomena: Kilo još ne podržava MCP autentifikaciju.', - "error.chain.providerAuthFailed": "Autentifikacija provajdera nije uspjela ({{provider}}): {{message}}", - "error.chain.providerInitFailed": - 'Neuspjelo inicijalizovanje provajdera "{{provider}}". Provjeri kredencijale i konfiguraciju.', - "error.chain.configJsonInvalid": "Konfiguracijska datoteka na {{path}} nije važeći JSON(C)", - "error.chain.configJsonInvalidWithMessage": "Konfiguracijska datoteka na {{path}} nije važeći JSON(C): {{message}}", - "error.chain.configDirectoryTypo": - 'Direktorij "{{dir}}" u {{path}} nije ispravan. Preimenuj direktorij u "{{suggestion}}" ili ga ukloni. Ovo je česta greška u kucanju.', - "error.chain.configFrontmatterError": "Neuspjelo parsiranje frontmatter-a u {{path}}:\n{{message}}", - "error.chain.configInvalid": "Konfiguracijska datoteka na {{path}} nije ispravna", - "error.chain.configInvalidWithMessage": "Konfiguracijska datoteka na {{path}} nije ispravna: {{message}}", - - "notification.permission.title": "Potrebna dozvola", - "notification.permission.description": "{{sessionTitle}} u {{projectName}} traži dozvolu", - "notification.question.title": "Pitanje", - "notification.question.description": "{{sessionTitle}} u {{projectName}} ima pitanje", - "notification.action.goToSession": "Idi na sesiju", - - "notification.session.responseReady.title": "Odgovor je spreman", - "notification.session.error.title": "Greška sesije", - "notification.session.error.fallbackDescription": "Došlo je do greške", - - "home.recentProjects": "Nedavni projekti", - "home.empty.title": "Nema nedavnih projekata", - "home.empty.description": "Kreni tako što ćeš otvoriti lokalni projekat", - - "session.tab.session": "Sesija", - "session.tab.review": "Pregled", - "session.tab.context": "Kontekst", - "session.panel.reviewAndFiles": "Pregled i datoteke", - "session.review.filesChanged": "Izmijenjeno {{count}} datoteka", - "session.review.change.one": "Izmjena", - "session.review.change.other": "Izmjene", - "session.review.loadingChanges": "Učitavanje izmjena...", - "session.review.empty": "Još nema izmjena u ovoj sesiji", - "session.review.noVcs": "Nije detektovan Git sistem kontrole verzija, promjene se ne prikazuju", - "session.review.noSnapshot": - "Praćenje snimaka (snapshot) je onemogućeno u konfiguraciji, pa promjene sesije nisu dostupne", - "session.review.noChanges": "Nema izmjena", - - "session.files.selectToOpen": "Odaberi datoteku za otvaranje", - "session.files.all": "Sve datoteke", - "session.files.empty": "Nema datoteka", - "session.files.binaryContent": "Binarna datoteka (sadržaj se ne može prikazati)", - - "session.messages.renderEarlier": "Prikaži ranije poruke", - "session.messages.loadingEarlier": "Učitavanje ranijih poruka...", - "session.messages.loadEarlier": "Učitaj ranije poruke", - "session.messages.loading": "Učitavanje poruka...", - "session.messages.jumpToLatest": "Idi na najnovije", - - "session.context.addToContext": "Dodaj {{selection}} u kontekst", - "session.todo.title": "Zadaci", - "session.todo.collapse": "Sažmi", - "session.todo.expand": "Proširi", - "session.followupDock.summary.one": "{{count}} poruka na čekanju", - "session.followupDock.summary.other": "{{count}} poruka na čekanju", - "session.followupDock.sendNow": "Pošalji sada", - "session.followupDock.edit": "Uredi", - "session.followupDock.collapse": "Sažmi poruke na čekanju", - "session.followupDock.expand": "Proširi poruke na čekanju", - "session.revertDock.summary.one": "{{count}} vraćena poruka", - "session.revertDock.summary.other": "{{count}} vraćenih poruka", - "session.revertDock.collapse": "Sažmi vraćene poruke", - "session.revertDock.expand": "Proširi vraćene poruke", - "session.revertDock.restore": "Vrati poruku", - - "session.new.title": "Napravi bilo šta", - "session.new.worktree.main": "Glavna grana", - "session.new.worktree.mainWithBranch": "Glavna grana ({{branch}})", - "session.new.worktree.create": "Kreiraj novi worktree", - "session.new.lastModified": "Posljednja izmjena", - - "session.header.search.placeholder": "Pretraži {{project}}", - "session.header.searchFiles": "Pretraži datoteke", - "session.header.openIn": "Otvori u", - "session.header.open.action": "Otvori {{app}}", - "session.header.open.ariaLabel": "Otvori u {{app}}", - "session.header.open.menu": "Opcije otvaranja", - "session.header.open.copyPath": "Kopiraj putanju", - - "status.popover.trigger": "Status", - "status.popover.ariaLabel": "Konfiguracije servera", - "status.popover.tab.servers": "Serveri", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "Plugini", - "status.popover.action.manageServers": "Upravljaj serverima", - - "session.share.popover.title": "Objavi na webu", - "session.share.popover.description.shared": "Ova sesija je javna na webu. Dostupna je svima koji imaju link.", - "session.share.popover.description.unshared": "Podijeli sesiju javno na webu. Biće dostupna svima koji imaju link.", - "session.share.action.share": "Podijeli", - "session.share.action.publish": "Objavi", - "session.share.action.publishing": "Objavljivanje...", - "session.share.action.unpublish": "Poništi objavu", - "session.share.action.unpublishing": "Poništavanje objave...", - "session.share.action.view": "Prikaži", - "session.share.copy.copied": "Kopirano", - "session.share.copy.copyLink": "Kopiraj link", - - "lsp.tooltip.none": "Nema LSP servera", - "lsp.label.connected": "{{count}} LSP", - - "prompt.loading": "Učitavanje upita...", - "terminal.loading": "Učitavanje terminala...", - "terminal.title": "Terminal", - "terminal.title.numbered": "Terminal {{number}}", - "terminal.close": "Zatvori terminal", - "terminal.connectionLost.title": "Veza prekinuta", - "terminal.connectionLost.description": - "Veza s terminalom je prekinuta. Ovo se može desiti kada se server restartuje.", - - "common.closeTab": "Zatvori karticu", - "common.dismiss": "Odbaci", - "common.requestFailed": "Zahtjev nije uspio", - "common.moreOptions": "Više opcija", - "common.learnMore": "Saznaj više", - "common.rename": "Preimenuj", - "common.reset": "Resetuj", - "common.archive": "Arhiviraj", - "common.delete": "Izbriši", - "common.close": "Zatvori", - "common.edit": "Uredi", - "common.loadMore": "Učitaj još", - "common.key.esc": "ESC", - - "sidebar.menu.toggle": "Prikaži/sakrij meni", - "sidebar.nav.projectsAndSessions": "Projekti i sesije", - "sidebar.settings": "Postavke", - "sidebar.help": "Pomoć", - "sidebar.workspaces.enable": "Omogući radne prostore", - "sidebar.workspaces.disable": "Onemogući radne prostore", - "sidebar.gettingStarted.title": "Početak", - "sidebar.gettingStarted.line1": "Kilo uključuje besplatne modele, tako da možeš odmah početi.", - "sidebar.gettingStarted.line2": "Poveži bilo kojeg provajdera da koristiš modele, npr. Claude, GPT, Gemini itd.", - "sidebar.project.recentSessions": "Nedavne sesije", - "sidebar.project.viewAllSessions": "Prikaži sve sesije", - "sidebar.project.clearNotifications": "Očisti obavijesti", - - "app.name.desktop": "Kilo Desktop", - - "settings.section.desktop": "Desktop", - "settings.section.server": "Server", - "settings.tab.general": "Opšte", - "settings.tab.shortcuts": "Prečice", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL integracija", - "settings.desktop.wsl.description": "Pokreni Kilo server unutar WSL-a na Windowsu.", - - "settings.general.section.appearance": "Izgled", - "settings.general.section.notifications": "Sistemske obavijesti", - "settings.general.section.updates": "Ažuriranja", - "settings.general.section.sounds": "Zvučni efekti", - "settings.general.section.feed": "Feed", - "settings.general.section.display": "Prikaz", - - "settings.general.row.language.title": "Jezik", - "settings.general.row.language.description": "Promijeni jezik prikaza u Kilo-u", - "settings.general.row.appearance.title": "Izgled", - "settings.general.row.appearance.description": "Prilagodi kako Kilo izgleda na tvom uređaju", - "settings.general.row.colorScheme.title": "Šema boja", - "settings.general.row.colorScheme.description": "Odaberi da li Kilo prati sistemsku, svijetlu ili tamnu temu", - "settings.general.row.theme.title": "Tema", - "settings.general.row.theme.description": "Prilagodi temu Kilo-a.", - "settings.general.row.font.title": "Font za kod", - "settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "UI font", - "settings.general.row.uiFont.description": "Prilagodi font koji se koristi u cijelom interfejsu", - "settings.general.row.followup.title": "Ponašanje nadovezivanja", - "settings.general.row.followup.description": "Odaberi da li upiti nadovezivanja usmjeravaju odmah ili čekaju u redu", - "settings.general.row.followup.option.queue": "Red čekanja", - "settings.general.row.followup.option.steer": "Usmjeri", - "settings.general.row.reasoningSummaries.title": "Prikaži sažetke rasuđivanja", - "settings.general.row.reasoningSummaries.description": "Prikaži sažetke rasuđivanja modela na vremenskoj traci", - - "settings.general.row.shellToolPartsExpanded.title": "Proširi dijelove shell alata", - "settings.general.row.shellToolPartsExpanded.description": - "Prikaži dijelove shell alata podrazumijevano proširene na vremenskoj traci", - "settings.general.row.editToolPartsExpanded.title": "Proširi dijelove alata za uređivanje", - "settings.general.row.editToolPartsExpanded.description": - "Prikaži dijelove alata za uređivanje, pisanje i patch podrazumijevano proširene na vremenskoj traci", - "settings.general.row.showSessionProgressBar.title": "Prikaži traku napretka sesije", - "settings.general.row.showSessionProgressBar.description": - "Prikaži animiranu traku napretka na vrhu sesije kada agent radi", - "settings.general.row.wayland.title": "Koristi nativni Wayland", - "settings.general.row.wayland.description": "Onemogući X11 fallback na Waylandu. Zahtijeva restart.", - "settings.general.row.wayland.tooltip": - "Na Linuxu sa monitorima miješanih stopa osvježavanja, nativni Wayland može biti stabilniji.", - - "settings.general.row.releaseNotes.title": "Bilješke o izdanju", - "settings.general.row.releaseNotes.description": 'Prikaži iskačuće prozore "Šta je novo" nakon ažuriranja', - - "settings.updates.row.startup.title": "Provjeri ažuriranja pri pokretanju", - "settings.updates.row.startup.description": "Automatski provjerava ažuriranja kada se Kilo pokrene", - "settings.updates.row.check.title": "Provjeri ažuriranja", - "settings.updates.row.check.description": "Ručno provjeri ažuriranja i instaliraj ako su dostupna", - "settings.updates.action.checkNow": "Provjeri sada", - "settings.updates.action.checking": "Provjera...", - "settings.updates.toast.latest.title": "Sve je ažurno", - "settings.updates.toast.latest.description": "Koristiš najnoviju verziju Kilo-a.", - "sound.option.none": "Nijedan", - "sound.option.alert01": "Upozorenje 01", - "sound.option.alert02": "Upozorenje 02", - "sound.option.alert03": "Upozorenje 03", - "sound.option.alert04": "Upozorenje 04", - "sound.option.alert05": "Upozorenje 05", - "sound.option.alert06": "Upozorenje 06", - "sound.option.alert07": "Upozorenje 07", - "sound.option.alert08": "Upozorenje 08", - "sound.option.alert09": "Upozorenje 09", - "sound.option.alert10": "Upozorenje 10", - "sound.option.bipbop01": "Bip-bop 01", - "sound.option.bipbop02": "Bip-bop 02", - "sound.option.bipbop03": "Bip-bop 03", - "sound.option.bipbop04": "Bip-bop 04", - "sound.option.bipbop05": "Bip-bop 05", - "sound.option.bipbop06": "Bip-bop 06", - "sound.option.bipbop07": "Bip-bop 07", - "sound.option.bipbop08": "Bip-bop 08", - "sound.option.bipbop09": "Bip-bop 09", - "sound.option.bipbop10": "Bip-bop 10", - "sound.option.staplebops01": "Staplebops 01", - "sound.option.staplebops02": "Staplebops 02", - "sound.option.staplebops03": "Staplebops 03", - "sound.option.staplebops04": "Staplebops 04", - "sound.option.staplebops05": "Staplebops 05", - "sound.option.staplebops06": "Staplebops 06", - "sound.option.staplebops07": "Staplebops 07", - "sound.option.nope01": "Ne 01", - "sound.option.nope02": "Ne 02", - "sound.option.nope03": "Ne 03", - "sound.option.nope04": "Ne 04", - "sound.option.nope05": "Ne 05", - "sound.option.nope06": "Ne 06", - "sound.option.nope07": "Ne 07", - "sound.option.nope08": "Ne 08", - "sound.option.nope09": "Ne 09", - "sound.option.nope10": "Ne 10", - "sound.option.nope11": "Ne 11", - "sound.option.nope12": "Ne 12", - "sound.option.yup01": "Da 01", - "sound.option.yup02": "Da 02", - "sound.option.yup03": "Da 03", - "sound.option.yup04": "Da 04", - "sound.option.yup05": "Da 05", - "sound.option.yup06": "Da 06", - - "settings.general.notifications.agent.title": "Agent", - "settings.general.notifications.agent.description": - "Prikaži sistemsku obavijest kada agent završi ili zahtijeva pažnju", - "settings.general.notifications.permissions.title": "Dozvole", - "settings.general.notifications.permissions.description": "Prikaži sistemsku obavijest kada je potrebna dozvola", - "settings.general.notifications.errors.title": "Greške", - "settings.general.notifications.errors.description": "Prikaži sistemsku obavijest kada dođe do greške", - - "settings.general.sounds.agent.title": "Agent", - "settings.general.sounds.agent.description": "Pusti zvuk kada agent završi ili zahtijeva pažnju", - "settings.general.sounds.permissions.title": "Dozvole", - "settings.general.sounds.permissions.description": "Pusti zvuk kada je potrebna dozvola", - "settings.general.sounds.errors.title": "Greške", - "settings.general.sounds.errors.description": "Pusti zvuk kada dođe do greške", - - "settings.shortcuts.title": "Prečice na tastaturi", - "settings.shortcuts.reset.button": "Vrati na podrazumijevano", - "settings.shortcuts.reset.toast.title": "Prečice resetovane", - "settings.shortcuts.reset.toast.description": "Prečice na tastaturi su vraćene na podrazumijevane.", - "settings.shortcuts.conflict.title": "Prečica je već u upotrebi", - "settings.shortcuts.conflict.description": "{{keybind}} je već dodijeljeno za {{titles}}.", - "settings.shortcuts.unassigned": "Nedodijeljeno", - "settings.shortcuts.pressKeys": "Pritisni tastere", - "settings.shortcuts.search.placeholder": "Pretraži prečice", - "settings.shortcuts.search.empty": "Nema pronađenih prečica", - - "settings.shortcuts.group.general": "Opšte", - "settings.shortcuts.group.session": "Sesija", - "settings.shortcuts.group.navigation": "Navigacija", - "settings.shortcuts.group.modelAndAgent": "Model i agent", - "settings.shortcuts.group.terminal": "Terminal", - "settings.shortcuts.group.prompt": "Upit", - - "settings.providers.title": "Provajderi", - "settings.providers.description": "Postavke provajdera će se ovdje moći podešavati.", - "settings.providers.section.connected": "Povezani provajderi", - "settings.providers.connected.empty": "Nema povezanih provajdera", - "settings.providers.section.popular": "Popularni provajderi", - "settings.providers.tag.environment": "Okruženje", - "settings.providers.tag.config": "Konfiguracija", - "settings.providers.tag.custom": "Prilagođeno", - "settings.providers.tag.other": "Ostalo", - "settings.models.title": "Modeli", - "settings.models.description": "Postavke modela će se ovdje moći podešavati.", - "settings.agents.title": "Agenti", - "settings.agents.description": "Postavke agenata će se ovdje moći podešavati.", - "settings.commands.title": "Komande", - "settings.commands.description": "Postavke komandi će se ovdje moći podešavati.", - "settings.mcp.title": "MCP", - "settings.mcp.description": "MCP postavke će se ovdje moći podešavati.", - - "settings.permissions.title": "Dozvole", - "settings.permissions.description": "Kontroliši koje alate server smije koristiti po defaultu.", - "settings.permissions.section.tools": "Alati", - "settings.permissions.toast.updateFailed.title": "Neuspjelo ažuriranje dozvola", - - "settings.permissions.action.allow": "Dozvoli", - "settings.permissions.action.ask": "Pitaj", - "settings.permissions.action.deny": "Zabrani", - - "settings.permissions.tool.read.title": "Čitanje", - "settings.permissions.tool.read.description": "Čitanje datoteke (podudara se s putanjom datoteke)", - "settings.permissions.tool.edit.title": "Uređivanje", - "settings.permissions.tool.edit.description": - "Mijenjanje datoteka, uključujući izmjene, pisanja, patch-eve i multi-izmjene", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "Podudaranje datoteka pomoću glob šablona", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "Pretraživanje sadržaja datoteka pomoću regularnih izraza", - "settings.permissions.tool.list.title": "Lista", - "settings.permissions.tool.list.description": "Listanje datoteka unutar direktorija", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "Pokretanje shell komandi", - "settings.permissions.tool.task.title": "Zadatak", - "settings.permissions.tool.task.description": "Pokretanje pod-agenta", - "settings.permissions.tool.skill.title": "Vještina", - "settings.permissions.tool.skill.description": "Učitaj vještinu po nazivu", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "Pokreni upite jezičnog servera", - "settings.permissions.tool.todowrite.title": "Ažuriranje liste zadataka", - "settings.permissions.tool.todowrite.description": "Ažuriraj listu zadataka", - "settings.permissions.tool.webfetch.title": "Web preuzimanje", - "settings.permissions.tool.webfetch.description": "Preuzmi sadržaj sa URL-a", - "settings.permissions.tool.websearch.title": "Web pretraga", - "settings.permissions.tool.websearch.description": "Pretražuj web", - "settings.permissions.tool.codesearch.title": "Pretraga koda", - "settings.permissions.tool.codesearch.description": "Pretraži kod na webu", - "settings.permissions.tool.external_directory.title": "Vanjski direktorij", - "settings.permissions.tool.external_directory.description": "Pristup datotekama izvan direktorija projekta", - "settings.permissions.tool.doom_loop.title": "Beskonačna petlja", - "settings.permissions.tool.doom_loop.description": "Otkriva ponovljene pozive alata sa identičnim unosom", - - "session.delete.failed.title": "Neuspjelo brisanje sesije", - "session.delete.title": "Izbriši sesiju", - "session.delete.confirm": 'Izbriši sesiju "{{name}}"?', - "session.delete.button": "Izbriši sesiju", - - "workspace.new": "Novi radni prostor", - "workspace.type.local": "lokalno", - "workspace.type.sandbox": "sandbox", - "workspace.create.failed.title": "Neuspješno kreiranje radnog prostora", - "workspace.delete.failed.title": "Neuspješno brisanje radnog prostora", - "workspace.resetting.title": "Resetovanje radnog prostora", - "workspace.resetting.description": "Ovo može potrajati minut.", - "workspace.reset.failed.title": "Neuspješno resetovanje radnog prostora", - "workspace.reset.success.title": "Radni prostor resetovan", - "workspace.reset.success.description": "Radni prostor sada odgovara podrazumijevanoj grani.", - "workspace.error.stillPreparing": "Radni prostor se još priprema", - "workspace.status.checking": "Provjera neobjedinjenih promjena...", - "workspace.status.error": "Nije moguće provjeriti git status.", - "workspace.status.clean": "Nisu pronađene neobjedinjene promjene.", - "workspace.status.dirty": "Pronađene su neobjedinjene promjene u ovom radnom prostoru.", - "workspace.delete.title": "Izbriši radni prostor", - "workspace.delete.confirm": 'Izbriši radni prostor "{{name}}"?', - "workspace.delete.button": "Izbriši radni prostor", - "workspace.reset.title": "Resetuj radni prostor", - "workspace.reset.confirm": 'Resetuj radni prostor "{{name}}"?', - "workspace.reset.button": "Resetuj radni prostor", - "workspace.reset.archived.none": "Nijedna aktivna sesija neće biti arhivirana.", - "workspace.reset.archived.one": "1 sesija će biti arhivirana.", - "workspace.reset.archived.many": "Biće arhivirano {{count}} sesija.", - "workspace.reset.note": "Ovo će resetovati radni prostor da odgovara podrazumijevanoj grani.", - "common.open": "Otvori", - "dialog.releaseNotes.action.getStarted": "Započni", - "dialog.releaseNotes.action.next": "Sljedeće", - "dialog.releaseNotes.action.hideFuture": "Ne prikazuj ovo u budućnosti", - "dialog.releaseNotes.media.alt": "Pregled izdanja", - "toast.project.reloadFailed.title": "Nije uspjelo ponovno učitavanje {{project}}", - "error.server.invalidConfiguration": "Nevažeća konfiguracija", - "common.moreCountSuffix": " (+{{count}} više)", - "common.time.justNow": "Upravo sada", - "common.time.minutesAgo.short": "prije {{count}} min", - "common.time.hoursAgo.short": "prije {{count}} h", - "common.time.daysAgo.short": "prije {{count}} d", - "settings.providers.connected.environmentDescription": "Povezano sa vašim varijablama okruženja", - "settings.providers.custom.description": "Dodajte provajdera kompatibilnog s OpenAI putem osnovnog URL-a.", - - "app.server.unreachable": "Nije moguće pristupiti {{server}}", - "app.server.retrying": "Automatski ponovni pokušaj...", - "app.server.otherServers": "Drugi serveri", - "dialog.server.add.usernamePlaceholder": "korisničko ime", - "dialog.server.add.passwordPlaceholder": "lozinka", - "server.row.noUsername": "nema korisničkog imena", - "session.review.noVcs.createGit.title": "Kreiraj Git repozitorij", - "session.review.noVcs.createGit.description": "Pratite, pregledajte i poništite promjene u ovom projektu", - "session.review.noVcs.createGit.actionLoading": "Kreiranje Git repozitorija...", - "session.review.noVcs.createGit.action": "Kreiraj Git repozitorij", - "session.todo.progress": "{{done}} od {{total}} zadataka završeno", - "session.question.progress": "{{current}} od {{total}} pitanja", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "File Explorer", - "session.header.open.fileManager": "File Manager", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "Terminal", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "Dijagnostika performansi razvoja", - "debugBar.na": "n/a", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": - "Posljednji završeni prelazak rute koji dotiče stranicu sesije, mjeren od početka rutera do prvog iscrtavanja nakon smirivanja.", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "Kadrovi u sekundi tokom posljednjih 5 sekundi.", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "Najgore vrijeme kadra u posljednjih 5 sekundi.", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "Kadrovi duži od 32ms u posljednjih 5 sekundi.", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "Blokirano vrijeme i broj dugih zadataka u posljednjih 5 sekundi. Maks zadatak: {{max}}.", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "Najgore zabilježeno kašnjenje unosa u posljednjih 5 sekundi.", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": - "Približno trajanje interakcije tokom posljednjih 5 sekundi. Ovo je slično INP-u, nije službeni Web Vitals INP.", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "Kumulativni pomak rasporeda za trenutni životni vijek aplikacije.", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "Korišteni JS heap naspram limita heapa. Samo Chromium.", - "debugBar.mem.tip": "Korišteni JS heap naspram limita heapa. {{used}} od {{limit}}.", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "Space", - "common.key.backspace": "Backspace", - "common.key.enter": "Enter", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "nepoznato", - "error.page.circular": "[Kružno]", - "error.globalSDK.noServerAvailable": "Nema dostupnog servera", - "error.globalSDK.serverNotAvailable": "Server nije dostupan", - "error.childStore.persistedCacheCreateFailed": "Nije uspjelo kreiranje trajnog keša", - "error.childStore.persistedProjectMetadataCreateFailed": "Nije uspjelo kreiranje trajnih metapodataka projekta", - "error.childStore.persistedProjectIconCreateFailed": "Nije uspjelo kreiranje trajne ikone projekta", - "error.childStore.storeCreateFailed": "Nije uspjelo kreiranje skladišta", - "terminal.connectionLost.abnormalClose": "WebSocket zatvoren nenormalno: {{code}}", -} diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts deleted file mode 100644 index a281c96e428..00000000000 --- a/packages/app/src/i18n/da.ts +++ /dev/null @@ -1,929 +0,0 @@ -export const dict = { - "command.category.suggested": "Foreslået", - "command.category.view": "Vis", - "command.category.project": "Projekt", - "command.category.provider": "Udbyder", - "command.category.server": "Server", - "command.category.session": "Session", - "command.category.theme": "Tema", - "command.category.language": "Sprog", - "command.category.file": "Fil", - "command.category.context": "Kontekst", - "command.category.terminal": "Terminal", - "command.category.model": "Model", - "command.category.mcp": "MCP", - "command.category.agent": "Agent", - "command.category.permissions": "Tilladelser", - "command.category.workspace": "Arbejdsområde", - - "command.category.settings": "Indstillinger", - "theme.scheme.system": "System", - "theme.scheme.light": "Lys", - "theme.scheme.dark": "Mørk", - - "command.sidebar.toggle": "Skift sidebjælke", - "command.project.open": "Åbn projekt", - "command.provider.connect": "Tilslut udbyder", - "command.server.switch": "Skift server", - "command.settings.open": "Åbn indstillinger", - "command.session.previous": "Forrige session", - "command.session.next": "Næste session", - "command.session.previous.unseen": "Forrige ulæste session", - "command.session.next.unseen": "Næste ulæste session", - "command.session.archive": "Arkivér session", - - "command.palette": "Kommandopalette", - - "command.theme.cycle": "Skift tema", - "command.theme.set": "Brug tema: {{theme}}", - "command.theme.scheme.cycle": "Skift farveskema", - "command.theme.scheme.set": "Brug farveskema: {{scheme}}", - - "command.language.cycle": "Skift sprog", - "command.language.set": "Brug sprog: {{language}}", - - "command.session.new": "Ny session", - "command.file.open": "Åbn fil", - "command.tab.close": "Luk fane", - "command.context.addSelection": "Tilføj markering til kontekst", - "command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil", - "command.input.focus": "Fokuser inputfelt", - "command.terminal.toggle": "Skift terminal", - "command.fileTree.toggle": "Skift filtræ", - "command.review.toggle": "Skift gennemgang", - "command.terminal.new": "Ny terminal", - "command.terminal.new.description": "Opret en ny terminalfane", - "command.steps.toggle": "Skift trin", - "command.steps.toggle.description": "Vis eller skjul trin for den aktuelle besked", - "command.message.previous": "Forrige besked", - "command.message.previous.description": "Gå til den forrige brugerbesked", - "command.message.next": "Næste besked", - "command.message.next.description": "Gå til den næste brugerbesked", - "command.model.choose": "Vælg model", - "command.model.choose.description": "Vælg en anden model", - "command.mcp.toggle": "Skift MCP'er", - "command.mcp.toggle.description": "Skift MCP'er", - "command.agent.cycle": "Skift agent", - "command.agent.cycle.description": "Skift til næste agent", - "command.agent.cycle.reverse": "Skift agent baglæns", - "command.agent.cycle.reverse.description": "Skift til forrige agent", - "command.model.variant.cycle": "Skift tænkeindsats", - "command.model.variant.cycle.description": "Skift til næste indsatsniveau", - "command.prompt.mode.shell": "Shell", - "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Accepter tilladelser automatisk", - "command.permissions.autoaccept.disable": "Stop med at acceptere tilladelser automatisk", - "command.workspace.toggle": "Skift arbejdsområder", - "command.workspace.toggle.description": "Aktiver eller deaktiver flere arbejdsområder i sidebjælken", - "command.session.undo": "Fortryd", - "command.session.undo.description": "Fortryd den sidste besked", - "command.session.redo": "Omgør", - "command.session.redo.description": "Omgør den sidste fortrudte besked", - "command.session.compact": "Komprimér session", - "command.session.compact.description": "Opsummer sessionen for at reducere kontekststørrelsen", - "command.session.fork": "Forgren fra besked", - "command.session.fork.description": "Opret en ny session fra en tidligere besked", - "command.session.share": "Del session", - "command.session.share.description": "Del denne session og kopier URL'en til udklipsholderen", - "command.session.unshare": "Stop deling af session", - "command.session.unshare.description": "Stop med at dele denne session", - - "palette.search.placeholder": "Søg i filer, kommandoer og sessioner", - "palette.empty": "Ingen resultater fundet", - "palette.group.commands": "Kommandoer", - "palette.group.files": "Filer", - - "dialog.provider.search.placeholder": "Søg udbydere", - "dialog.provider.empty": "Ingen udbydere fundet", - "dialog.provider.group.popular": "Populære", - "dialog.provider.group.other": "Andre", - "dialog.provider.tag.recommended": "Anbefalet", - "dialog.provider.opencode.note": "Udvalgte modeller inklusive Claude, GPT, Gemini og flere", - "dialog.provider.opencode.tagline": "Pålidelige optimerede modeller", - "dialog.provider.opencodeGo.tagline": "Billigt abonnement for alle", - "dialog.provider.anthropic.note": "Direkte adgang til Claude-modeller, inklusive Pro og Max", - "dialog.provider.copilot.note": "AI-modeller til kodningsassistance via GitHub Copilot", - "dialog.provider.openai.note": "GPT-modeller til hurtige, kompetente generelle AI-opgaver", - "dialog.provider.google.note": "Gemini-modeller til hurtige, strukturerede svar", - "dialog.provider.openrouter.note": "Få adgang til alle understøttede modeller fra én udbyder", - "dialog.provider.vercel.note": "Samlet adgang til AI-modeller med smart routing", - - "dialog.model.select.title": "Vælg model", - "dialog.model.search.placeholder": "Søg modeller", - "dialog.model.empty": "Ingen modeller fundet", - "dialog.model.manage": "Administrer modeller", - "dialog.model.manage.description": "Tilpas hvilke modeller der vises i modelvælgeren.", - "dialog.model.manage.provider.toggle": "Skift alle {{provider}}-modeller", - - "dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af Kilo", - "dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere", - - "dialog.provider.viewAll": "Vis flere udbydere", - - "provider.connect.title": "Forbind {{provider}}", - "provider.connect.title.anthropicProMax": "Log ind med Claude Pro/Max", - "provider.connect.selectMethod": "Vælg loginmetode for {{provider}}.", - "provider.connect.method.apiKey": "API-nøgle", - "provider.connect.status.inProgress": "Godkendelse i gang...", - "provider.connect.status.waiting": "Venter på godkendelse...", - "provider.connect.status.failed": "Godkendelse mislykkedes: {{error}}", - "provider.connect.apiKey.description": - "Indtast din {{provider}} API-nøgle for at forbinde din konto og bruge {{provider}} modeller i Kilo.", - "provider.connect.apiKey.label": "{{provider}} API-nøgle", - "provider.connect.apiKey.placeholder": "API-nøgle", - "provider.connect.apiKey.required": "API-nøgle er påkrævet", - "provider.connect.opencodeZen.line1": - "OpenCode Zen giver dig adgang til et udvalg af pålidelige optimerede modeller til kodningsagenter.", - "provider.connect.opencodeZen.line2": - "Med en enkelt API-nøgle får du adgang til modeller som Claude, GPT, Gemini, GLM og flere.", - "provider.connect.opencodeZen.visit.prefix": "Besøg ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " for at hente din API-nøgle.", - "provider.connect.oauth.code.visit.prefix": "Besøg ", - "provider.connect.oauth.code.visit.link": "dette link", - "provider.connect.oauth.code.visit.suffix": - " for at hente din godkendelseskode for at forbinde din konto og bruge {{provider}} modeller i Kilo.", - "provider.connect.oauth.code.label": "{{method}} godkendelseskode", - "provider.connect.oauth.code.placeholder": "Godkendelseskode", - "provider.connect.oauth.code.required": "Godkendelseskode er påkrævet", - "provider.connect.oauth.code.invalid": "Ugyldig godkendelseskode", - "provider.connect.oauth.auto.visit.prefix": "Besøg ", - "provider.connect.oauth.auto.visit.link": "dette link", - "provider.connect.oauth.auto.visit.suffix": - " og indtast koden nedenfor for at forbinde din konto og bruge {{provider}} modeller i Kilo.", - "provider.connect.oauth.auto.confirmationCode": "Bekræftelseskode", - "provider.connect.toast.connected.title": "{{provider}} forbundet", - "provider.connect.toast.connected.description": "{{provider}} modeller er nu tilgængelige.", - - "provider.custom.title": "Brugerdefineret udbyder", - "provider.custom.description.prefix": "Konfigurer en OpenAI-kompatibel udbyder. Se ", - "provider.custom.description.link": "dokumentation for udbyderkonfiguration", - "provider.custom.description.suffix": ".", - "provider.custom.field.providerID.label": "Udbyder-ID", - "provider.custom.field.providerID.placeholder": "minudbyder", - "provider.custom.field.providerID.description": "Små bogstaver, tal, bindestreger eller understregninger", - "provider.custom.field.name.label": "Visningsnavn", - "provider.custom.field.name.placeholder": "Min AI-udbyder", - "provider.custom.field.baseURL.label": "Basis-URL", - "provider.custom.field.baseURL.placeholder": "https://api.minudbyder.dk/v1", - "provider.custom.field.apiKey.label": "API-nøgle", - "provider.custom.field.apiKey.placeholder": "API-nøgle", - "provider.custom.field.apiKey.description": "Valgfri. Lad være tom, hvis du administrerer godkendelse via headers.", - "provider.custom.models.label": "Modeller", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "model-id", - "provider.custom.models.name.label": "Navn", - "provider.custom.models.name.placeholder": "Visningsnavn", - "provider.custom.models.remove": "Fjern model", - "provider.custom.models.add": "Tilføj model", - "provider.custom.headers.label": "Headers (valgfri)", - "provider.custom.headers.key.label": "Header", - "provider.custom.headers.key.placeholder": "Header-Navn", - "provider.custom.headers.value.label": "Værdi", - "provider.custom.headers.value.placeholder": "værdi", - "provider.custom.headers.remove": "Fjern header", - "provider.custom.headers.add": "Tilføj header", - "provider.custom.error.providerID.required": "Udbyder-ID er påkrævet", - "provider.custom.error.providerID.format": "Brug små bogstaver, tal, bindestreger eller understregninger", - "provider.custom.error.providerID.exists": "Dette udbyder-ID findes allerede", - "provider.custom.error.name.required": "Visningsnavn er påkrævet", - "provider.custom.error.baseURL.required": "Basis-URL er påkrævet", - "provider.custom.error.baseURL.format": "Skal starte med http:// eller https://", - "provider.custom.error.required": "Påkrævet", - "provider.custom.error.duplicate": "Duplikeret", - - "provider.disconnect.toast.disconnected.title": "{{provider}} frakoblet", - "provider.disconnect.toast.disconnected.description": "Modeller fra {{provider}} er ikke længere tilgængelige.", - "model.tag.free": "Gratis", - "model.tag.latest": "Nyeste", - - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "tekst", - "model.input.image": "billede", - "model.input.audio": "lyd", - "model.input.video": "video", - "model.input.pdf": "pdf", - "model.tooltip.allows": "Tillader: {{inputs}}", - "model.tooltip.reasoning.allowed": "Tillader tænkning", - "model.tooltip.reasoning.none": "Ingen tænkning", - "model.tooltip.context": "Kontekstgrænse {{limit}}", - "common.search.placeholder": "Søg", - "common.goBack": "Gå tilbage", - "common.goForward": "Naviger fremad", - "common.loading": "Indlæser", - "common.loading.ellipsis": "...", - "common.cancel": "Annuller", - "common.connect": "Forbind", - "common.disconnect": "Frakobl", - "common.continue": "Indsend", - "common.submit": "Indsend", - "common.save": "Gem", - "common.saving": "Gemmer...", - "common.default": "Standard", - "common.attachment": "vedhæftning", - - "prompt.placeholder.shell": "Indtast shell-kommando... {{example}}", - "prompt.placeholder.normal": 'Spørg om hvad som helst... "{{example}}"', - "prompt.placeholder.simple": "Spørg om hvad som helst...", - "prompt.placeholder.summarizeComments": "Opsummér kommentarer…", - "prompt.placeholder.summarizeComment": "Opsummér kommentar…", - "prompt.mode.shell": "Shell", - "prompt.mode.normal": "Prompt", - "prompt.mode.shell.exit": "esc for at afslutte", - - "prompt.example.1": "Ret en TODO i koden", - "prompt.example.2": "Hvad er teknologistakken for dette projekt?", - "prompt.example.3": "Ret ødelagte tests", - "prompt.example.4": "Forklar hvordan godkendelse fungerer", - "prompt.example.5": "Find og ret sikkerhedshuller", - "prompt.example.6": "Tilføj enhedstests for brugerservice", - "prompt.example.7": "Refaktorer denne funktion så den er mere læsbar", - "prompt.example.8": "Hvad betyder denne fejl?", - "prompt.example.9": "Hjælp mig med at debugge dette problem", - "prompt.example.10": "Generer API-dokumentation", - "prompt.example.11": "Optimer databaseforespørgsler", - "prompt.example.12": "Tilføj validering af input", - "prompt.example.13": "Opret en ny komponent til...", - "prompt.example.14": "Hvordan deployerer jeg dette projekt?", - "prompt.example.15": "Gennemgå min kode for bedste praksis", - "prompt.example.16": "Tilføj fejlhåndtering til denne funktion", - "prompt.example.17": "Forklar dette regex-mønster", - "prompt.example.18": "Konverter dette til TypeScript", - "prompt.example.19": "Tilføj logning i hele koden", - "prompt.example.20": "Hvilke afhængigheder er forældede?", - "prompt.example.21": "Hjælp mig med at skrive et migreringsscript", - "prompt.example.22": "Implementer caching for dette endpoint", - "prompt.example.23": "Tilføj sideinddeling til denne liste", - "prompt.example.24": "Opret en CLI-kommando til...", - "prompt.example.25": "Hvordan fungerer miljøvariabler her?", - - "prompt.popover.emptyResults": "Ingen matchende resultater", - "prompt.popover.emptyCommands": "Ingen matchende kommandoer", - "prompt.dropzone.label": "Slip billeder, PDF'er eller tekstfiler her", - "prompt.dropzone.file.label": "Slip for at @nævne fil", - "prompt.slash.badge.custom": "brugerdefineret", - "prompt.slash.badge.skill": "skill", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "aktiv", - "prompt.context.includeActiveFile": "Inkluder aktiv fil", - "prompt.context.removeActiveFile": "Fjern aktiv fil fra kontekst", - "prompt.context.removeFile": "Fjern fil fra kontekst", - "prompt.action.attachFile": "Vedhæft fil", - "prompt.attachment.remove": "Fjern vedhæftning", - "prompt.action.send": "Send", - "prompt.action.stop": "Stop", - - "prompt.toast.pasteUnsupported.title": "Ikke understøttet vedhæftning", - "prompt.toast.pasteUnsupported.description": "Kun billeder, PDF'er eller tekstfiler kan vedhæftes her.", - "prompt.toast.modelAgentRequired.title": "Vælg en agent og model", - "prompt.toast.modelAgentRequired.description": "Vælg en agent og model før du sender en forespørgsel.", - "prompt.toast.worktreeCreateFailed.title": "Kunne ikke oprette worktree", - "prompt.toast.sessionCreateFailed.title": "Kunne ikke oprette session", - "prompt.toast.shellSendFailed.title": "Kunne ikke sende shell-kommando", - "prompt.toast.commandSendFailed.title": "Kunne ikke sende kommando", - "prompt.toast.promptSendFailed.title": "Kunne ikke sende forespørgsel", - "prompt.toast.promptSendFailed.description": "Kunne ikke hente session", - - "dialog.mcp.title": "MCP'er", - "dialog.mcp.description": "{{enabled}} af {{total}} aktiveret", - "dialog.mcp.empty": "Ingen MCP'er konfigureret", - - "dialog.lsp.empty": "LSP'er registreret automatisk fra filtyper", - "dialog.plugins.empty": "Plugins konfigureret i opencode.json", - - "mcp.status.connected": "forbundet", - "mcp.status.failed": "mislykkedes", - "mcp.status.needs_auth": "kræver godkendelse", - "mcp.status.disabled": "deaktiveret", - - "dialog.fork.empty": "Ingen beskeder at forgrene fra", - - "dialog.directory.search.placeholder": "Søg mapper", - "dialog.directory.empty": "Ingen mapper fundet", - - "dialog.server.title": "Servere", - "dialog.server.description": "Skift hvilken Kilo-server denne app forbinder til.", - "dialog.server.search.placeholder": "Søg servere", - "dialog.server.empty": "Ingen servere endnu", - "dialog.server.add.title": "Tilføj en server", - "dialog.server.add.url": "Server URL", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "Kunne ikke forbinde til server", - "dialog.server.add.checking": "Tjekker...", - "dialog.server.add.button": "Tilføj server", - "dialog.server.add.name": "Servernavn (valgfrit)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "Brugernavn (valgfrit)", - "dialog.server.add.password": "Adgangskode (valgfrit)", - "dialog.server.edit.title": "Rediger server", - "dialog.server.default.title": "Standardserver", - "dialog.server.default.description": - "Forbind til denne server ved start af app i stedet for at starte en lokal server. Kræver genstart.", - "dialog.server.default.none": "Ingen server valgt", - "dialog.server.default.set": "Sæt nuværende server som standard", - "dialog.server.default.clear": "Ryd", - "dialog.server.action.remove": "Fjern server", - - "dialog.server.menu.edit": "Rediger", - "dialog.server.menu.default": "Sæt som standard", - "dialog.server.menu.defaultRemove": "Fjern som standard", - "dialog.server.menu.delete": "Slet", - "dialog.server.current": "Nuværende server", - "dialog.server.status.default": "Standard", - - "dialog.project.edit.title": "Rediger projekt", - "dialog.project.edit.name": "Navn", - "dialog.project.edit.icon": "Ikon", - "dialog.project.edit.icon.alt": "Projektikon", - "dialog.project.edit.icon.hint": "Klik eller træk et billede", - "dialog.project.edit.icon.recommended": "Anbefalet: 128x128px", - "dialog.project.edit.color": "Farve", - "dialog.project.edit.color.select": "Vælg farven {{color}}", - - "dialog.project.edit.worktree.startup": "Opstartsscript for arbejdsområde", - "dialog.project.edit.worktree.startup.description": "Køres efter oprettelse af et nyt arbejdsområde (worktree).", - "dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install", - "context.breakdown.title": "Kontekstfordeling", - "context.breakdown.note": - 'Omtrentlig fordeling af input-tokens. "Andre" inkluderer værktøjsdefinitioner og overhead.', - "context.breakdown.system": "System", - "context.breakdown.user": "Bruger", - "context.breakdown.assistant": "Assistent", - "context.breakdown.tool": "Værktøjskald", - "context.breakdown.other": "Andre", - - "context.systemPrompt.title": "Systemprompt", - "context.rawMessages.title": "Rå beskeder", - - "context.stats.session": "Session", - "context.stats.messages": "Beskeder", - "context.stats.provider": "Udbyder", - "context.stats.model": "Model", - "context.stats.limit": "Kontekstgrænse", - "context.stats.totalTokens": "Samlede tokens", - "context.stats.usage": "Forbrug", - "context.stats.inputTokens": "Input-tokens", - "context.stats.outputTokens": "Output-tokens", - "context.stats.reasoningTokens": "Tænke Tokens", - "context.stats.cacheTokens": "Cache Tokens (læs/skriv)", - "context.stats.userMessages": "Brugerbeskeder", - "context.stats.assistantMessages": "Assistentbeskeder", - "context.stats.totalCost": "Samlede omkostninger", - "context.stats.sessionCreated": "Session oprettet", - "context.stats.lastActivity": "Seneste aktivitet", - - "context.usage.tokens": "Tokens", - "context.usage.usage": "Forbrug", - "context.usage.cost": "Omkostning", - "context.usage.clickToView": "Klik for at se kontekst", - "context.usage.view": "Se kontekstforbrug", - - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - - "toast.language.title": "Sprog", - "toast.language.description": "Skiftede til {{language}}", - - "toast.theme.title": "Tema skiftet", - "toast.scheme.title": "Farveskema", - - "toast.permissions.autoaccept.on.title": "Accepterer tilladelser automatisk", - "toast.permissions.autoaccept.on.description": "Anmodninger om tilladelse godkendes automatisk", - "toast.permissions.autoaccept.off.title": "Stoppet med at acceptere tilladelser automatisk", - "toast.permissions.autoaccept.off.description": "Anmodninger om tilladelse vil kræve godkendelse", - - "toast.workspace.enabled.title": "Arbejdsområder aktiveret", - "toast.workspace.enabled.description": "Flere worktrees vises nu i sidepanelet", - "toast.workspace.disabled.title": "Arbejdsområder deaktiveret", - "toast.workspace.disabled.description": "Kun hoved-worktree vises i sidepanelet", - - "toast.model.none.title": "Ingen model valgt", - "toast.model.none.description": "Forbind en udbyder for at opsummere denne session", - - "toast.file.loadFailed.title": "Kunne ikke indlæse fil", - - "toast.file.listFailed.title": "Kunne ikke liste filer", - "toast.context.noLineSelection.title": "Ingen linjevalg", - "toast.context.noLineSelection.description": "Vælg først et linjeinterval i en filfane.", - "toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til udklipsholder", - "toast.session.share.success.title": "Session delt", - "toast.session.share.success.description": "Delings-URL kopieret til udklipsholder!", - "toast.session.share.failed.title": "Kunne ikke dele session", - "toast.session.share.failed.description": "Der opstod en fejl under deling af sessionen", - - "toast.session.unshare.success.title": "Deling af session stoppet", - "toast.session.unshare.success.description": "Deling af session blev stoppet!", - "toast.session.unshare.failed.title": "Kunne ikke stoppe deling af session", - "toast.session.unshare.failed.description": "Der opstod en fejl under stop af sessionsdeling", - - "toast.session.listFailed.title": "Kunne ikke indlæse sessioner for {{project}}", - - "toast.update.title": "Opdatering tilgængelig", - "toast.update.description": "En ny version af Kilo ({{version}}) er nu tilgængelig til installation.", - "toast.update.action.installRestart": "Installer og genstart", - "toast.update.action.notYet": "Ikke endnu", - - "error.page.title": "Noget gik galt", - "error.page.description": "Der opstod en fejl under indlæsning af applikationen.", - "error.page.details.label": "Fejldetaljer", - "error.page.action.restart": "Genstart", - "error.page.action.checking": "Tjekker...", - "error.page.action.checkUpdates": "Tjek for opdateringer", - "error.page.action.updateTo": "Opdater til {{version}}", - "error.page.report.prefix": "Rapporter venligst denne fejl til Kilo-teamet", - "error.page.report.discord": "på Discord", - "error.page.version": "Version: {{version}}", - - "error.dev.rootNotFound": - "Rodelement ikke fundet. Har du glemt at tilføje det til din index.html? Eller måske er id-attributten stavet forkert?", - - "error.globalSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?", - "directory.error.invalidUrl": "Ugyldig mappe i URL.", - - "error.chain.unknown": "Ukendt fejl", - "error.chain.causedBy": "Forårsaget af:", - "error.chain.apiError": "API-fejl", - "error.chain.status": "Status: {{status}}", - "error.chain.retryable": "Kan forsøges igen: {{retryable}}", - "error.chain.responseBody": "Svarindhold:\n{{body}}", - "error.chain.didYouMean": "Mente du: {{suggestions}}", - "error.chain.modelNotFound": "Model ikke fundet: {{provider}}/{{model}}", - "error.chain.checkConfig": "Tjek dine konfigurations (opencode.json) udbyder/modelnavne", - "error.chain.mcpFailed": 'MCP-server "{{name}}" fejlede. Bemærk, Kilo understøtter ikke MCP-godkendelse endnu.', - "error.chain.providerAuthFailed": "Udbydergodkendelse mislykkedes ({{provider}}): {{message}}", - "error.chain.providerInitFailed": - 'Kunne ikke initialisere udbyder "{{provider}}". Tjek legitimationsoplysninger og konfiguration.', - "error.chain.configJsonInvalid": "Konfigurationsfil på {{path}} er ikke gyldig JSON(C)", - "error.chain.configJsonInvalidWithMessage": "Konfigurationsfil på {{path}} er ikke gyldig JSON(C): {{message}}", - "error.chain.configDirectoryTypo": - 'Mappe "{{dir}}" i {{path}} er ikke gyldig. Omdøb mappen til "{{suggestion}}" eller fjern den. Dette er en almindelig slåfejl.', - "error.chain.configFrontmatterError": "Kunne ikke parse frontmatter i {{path}}:\n{{message}}", - "error.chain.configInvalid": "Konfigurationsfil på {{path}} er ugyldig", - "error.chain.configInvalidWithMessage": "Konfigurationsfil på {{path}} er ugyldig: {{message}}", - - "notification.permission.title": "Tilladelse påkrævet", - "notification.permission.description": "{{sessionTitle}} i {{projectName}} kræver tilladelse", - "notification.question.title": "Spørgsmål", - "notification.question.description": "{{sessionTitle}} i {{projectName}} har et spørgsmål", - "notification.action.goToSession": "Gå til session", - - "notification.session.responseReady.title": "Svar klar", - "notification.session.error.title": "Sessionsfejl", - "notification.session.error.fallbackDescription": "Der opstod en fejl", - - "home.recentProjects": "Seneste projekter", - "home.empty.title": "Ingen seneste projekter", - "home.empty.description": "Kom i gang ved at åbne et lokalt projekt", - - "session.tab.session": "Session", - "session.tab.review": "Gennemgang", - "session.tab.context": "Kontekst", - "session.panel.reviewAndFiles": "Gennemgang og filer", - "session.review.filesChanged": "{{count}} Filer ændret", - "session.review.change.one": "Ændring", - "session.review.change.other": "Ændringer", - "session.review.loadingChanges": "Indlæser ændringer...", - "session.review.empty": "Ingen ændringer i denne session endnu", - "session.review.noVcs": "Intet Git versionsstyringssystem fundet, ændringer vises ikke", - "session.review.noSnapshot": - "Snapshot-sporing er deaktiveret i konfigurationen, så sessionsændringer er ikke tilgængelige", - "session.review.noChanges": "Ingen ændringer", - "session.files.selectToOpen": "Vælg en fil at åbne", - "session.files.all": "Alle filer", - "session.files.empty": "Ingen filer", - "session.files.binaryContent": "Binær fil (indhold kan ikke vises)", - "session.messages.renderEarlier": "Vis tidligere beskeder", - "session.messages.loadingEarlier": "Indlæser tidligere beskeder...", - "session.messages.loadEarlier": "Indlæs tidligere beskeder", - "session.messages.loading": "Indlæser beskeder...", - - "session.messages.jumpToLatest": "Gå til seneste", - "session.context.addToContext": "Tilføj {{selection}} til kontekst", - "session.todo.title": "Opgaver", - "session.todo.collapse": "Skjul", - "session.todo.expand": "Udvid", - "session.followupDock.summary.one": "{{count}} besked i kø", - "session.followupDock.summary.other": "{{count}} beskeder i kø", - "session.followupDock.sendNow": "Send nu", - "session.followupDock.edit": "Rediger", - "session.followupDock.collapse": "Skjul beskeder i kø", - "session.followupDock.expand": "Udvid beskeder i kø", - "session.revertDock.summary.one": "{{count}} tilbagerullet besked", - "session.revertDock.summary.other": "{{count}} tilbagerullede beskeder", - "session.revertDock.collapse": "Skjul tilbagerullede beskeder", - "session.revertDock.expand": "Udvid tilbagerullede beskeder", - "session.revertDock.restore": "Gendan besked", - - "session.new.title": "Byg hvad som helst", - "session.new.worktree.main": "Hovedgren", - "session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})", - "session.new.worktree.create": "Opret nyt worktree", - "session.new.lastModified": "Sidst ændret", - - "session.header.search.placeholder": "Søg {{project}}", - "session.header.searchFiles": "Søg efter filer", - "session.header.openIn": "Åbn i", - "session.header.open.action": "Åbn {{app}}", - "session.header.open.ariaLabel": "Åbn i {{app}}", - "session.header.open.menu": "Åbningsmuligheder", - "session.header.open.copyPath": "Kopier sti", - - "status.popover.trigger": "Status", - "status.popover.ariaLabel": "Serverkonfigurationer", - "status.popover.tab.servers": "Servere", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "Plugins", - "status.popover.action.manageServers": "Administrer servere", - - "session.share.popover.title": "Udgiv på nettet", - "session.share.popover.description.shared": - "Denne session er offentlig på nettet. Den er tilgængelig for alle med linket.", - "session.share.popover.description.unshared": - "Del session offentligt på nettet. Den vil være tilgængelig for alle med linket.", - "session.share.action.share": "Del", - "session.share.action.publish": "Udgiv", - "session.share.action.publishing": "Udgiver...", - "session.share.action.unpublish": "Afpublicer", - "session.share.action.unpublishing": "Afpublicerer...", - "session.share.action.view": "Vis", - "session.share.copy.copied": "Kopieret", - "session.share.copy.copyLink": "Kopier link", - - "lsp.tooltip.none": "Ingen LSP-servere", - "lsp.label.connected": "{{count}} LSP", - - "prompt.loading": "Indlæser prompt...", - "terminal.loading": "Indlæser terminal...", - "terminal.title": "Terminal", - "terminal.title.numbered": "Terminal {{number}}", - "terminal.close": "Luk terminal", - - "terminal.connectionLost.title": "Forbindelse mistet", - "terminal.connectionLost.description": "Terminalforbindelsen blev afbrudt. Dette kan ske, når serveren genstarter.", - "common.closeTab": "Luk fane", - "common.dismiss": "Afvis", - "common.requestFailed": "Forespørgsel mislykkedes", - "common.moreOptions": "Flere muligheder", - "common.learnMore": "Lær mere", - "common.rename": "Omdøb", - "common.reset": "Nulstil", - "common.archive": "Arkivér", - "common.delete": "Slet", - "common.close": "Luk", - "common.edit": "Rediger", - "common.loadMore": "Indlæs flere", - - "common.key.esc": "ESC", - "sidebar.menu.toggle": "Skift menu", - "sidebar.nav.projectsAndSessions": "Projekter og sessioner", - "sidebar.settings": "Indstillinger", - "sidebar.help": "Hjælp", - "sidebar.workspaces.enable": "Aktiver arbejdsområder", - "sidebar.workspaces.disable": "Deaktiver arbejdsområder", - "sidebar.gettingStarted.title": "Kom i gang", - "sidebar.gettingStarted.line1": "Kilo inkluderer gratis modeller så du kan starte med det samme.", - "sidebar.gettingStarted.line2": "Forbind enhver udbyder for at bruge modeller, inkl. Claude, GPT, Gemini osv.", - "sidebar.project.recentSessions": "Seneste sessioner", - "sidebar.project.viewAllSessions": "Vis alle sessioner", - "sidebar.project.clearNotifications": "Ryd notifikationer", - - "app.name.desktop": "Kilo Desktop", - "settings.section.desktop": "Desktop", - "settings.section.server": "Server", - "settings.tab.general": "Generelt", - "settings.tab.shortcuts": "Genveje", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL integration", - "settings.desktop.wsl.description": "Kør Kilo-serveren inde i WSL på Windows.", - - "settings.general.section.appearance": "Udseende", - "settings.general.section.notifications": "Systemmeddelelser", - "settings.general.section.updates": "Opdateringer", - "settings.general.section.sounds": "Lydeffekter", - "settings.general.section.feed": "Feed", - "settings.general.section.display": "Skærm", - - "settings.general.row.language.title": "Sprog", - "settings.general.row.language.description": "Ændr visningssproget for Kilo", - "settings.general.row.appearance.title": "Udseende", - "settings.general.row.appearance.description": "Tilpas hvordan Kilo ser ud på din enhed", - "settings.general.row.colorScheme.title": "Farveskema", - "settings.general.row.colorScheme.description": "Vælg om Kilo følger systemets, lyst eller mørkt tema", - "settings.general.row.theme.title": "Tema", - "settings.general.row.theme.description": "Tilpas hvordan Kilo er temabestemt.", - "settings.general.row.font.title": "Kode-skrifttype", - "settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "UI-skrifttype", - "settings.general.row.uiFont.description": "Tilpas skrifttypen, der bruges i hele brugerfladen", - "settings.general.row.followup.title": "Opfølgningsadfærd", - "settings.general.row.followup.description": "Vælg om opfølgende forespørgsler skal styre straks eller vente i kø", - "settings.general.row.followup.option.queue": "Kø", - "settings.general.row.followup.option.steer": "Styr", - "settings.general.row.reasoningSummaries.title": "Vis tænkeoversigter", - "settings.general.row.reasoningSummaries.description": "Vis model tænkeoversigter i tidslinjen", - - "settings.general.row.shellToolPartsExpanded.title": "Udvid shell-værktøjsdele", - "settings.general.row.shellToolPartsExpanded.description": "Vis shell-værktøjsdele udvidet som standard i tidslinjen", - "settings.general.row.editToolPartsExpanded.title": "Udvid edit-værktøjsdele", - "settings.general.row.editToolPartsExpanded.description": - "Vis edit-, write- og patch-værktøjsdele udvidet som standard i tidslinjen", - "settings.general.row.showSessionProgressBar.title": "Vis sessionens fremdriftslinje", - "settings.general.row.showSessionProgressBar.description": - "Vis den animerede fremdriftslinje øverst i sessionen, når agenten arbejder", - "settings.general.row.wayland.title": "Brug native Wayland", - "settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Kræver genstart.", - "settings.general.row.wayland.tooltip": - "På Linux med skærme med blandet opdateringshastighed kan native Wayland være mere stabilt.", - - "settings.general.row.releaseNotes.title": "Udgivelsesnoter", - "settings.general.row.releaseNotes.description": 'Vis "Hvad er nyt"-popups efter opdateringer', - - "settings.updates.row.startup.title": "Tjek for opdateringer ved opstart", - "settings.updates.row.startup.description": "Tjek automatisk for opdateringer, når Kilo starter", - "settings.updates.row.check.title": "Tjek for opdateringer", - "settings.updates.row.check.description": "Tjek manuelt for opdateringer og installer, hvis tilgængelig", - "settings.updates.action.checkNow": "Tjek nu", - "settings.updates.action.checking": "Tjekker...", - "settings.updates.toast.latest.title": "Du er opdateret", - "settings.updates.toast.latest.description": "Du kører den nyeste version af Kilo.", - - "sound.option.none": "Ingen", - "sound.option.alert01": "Alarm 01", - "sound.option.alert02": "Alarm 02", - "sound.option.alert03": "Alarm 03", - "sound.option.alert04": "Alarm 04", - "sound.option.alert05": "Alarm 05", - "sound.option.alert06": "Alarm 06", - "sound.option.alert07": "Alarm 07", - "sound.option.alert08": "Alarm 08", - "sound.option.alert09": "Alarm 09", - "sound.option.alert10": "Alarm 10", - "sound.option.bipbop01": "Bip-bop 01", - "sound.option.bipbop02": "Bip-bop 02", - "sound.option.bipbop03": "Bip-bop 03", - "sound.option.bipbop04": "Bip-bop 04", - "sound.option.bipbop05": "Bip-bop 05", - "sound.option.bipbop06": "Bip-bop 06", - "sound.option.bipbop07": "Bip-bop 07", - "sound.option.bipbop08": "Bip-bop 08", - "sound.option.bipbop09": "Bip-bop 09", - "sound.option.bipbop10": "Bip-bop 10", - "sound.option.staplebops01": "Staplebops 01", - "sound.option.staplebops02": "Staplebops 02", - "sound.option.staplebops03": "Staplebops 03", - "sound.option.staplebops04": "Staplebops 04", - "sound.option.staplebops05": "Staplebops 05", - "sound.option.staplebops06": "Staplebops 06", - "sound.option.staplebops07": "Staplebops 07", - "sound.option.nope01": "Nej 01", - "sound.option.nope02": "Nej 02", - "sound.option.nope03": "Nej 03", - "sound.option.nope04": "Nej 04", - "sound.option.nope05": "Nej 05", - "sound.option.nope06": "Nej 06", - "sound.option.nope07": "Nej 07", - "sound.option.nope08": "Nej 08", - "sound.option.nope09": "Nej 09", - "sound.option.nope10": "Nej 10", - "sound.option.nope11": "Nej 11", - "sound.option.nope12": "Nej 12", - "sound.option.yup01": "Ja 01", - "sound.option.yup02": "Ja 02", - "sound.option.yup03": "Ja 03", - "sound.option.yup04": "Ja 04", - "sound.option.yup05": "Ja 05", - "sound.option.yup06": "Ja 06", - "settings.general.notifications.agent.title": "Agent", - "settings.general.notifications.agent.description": - "Vis systemmeddelelse når agenten er færdig eller kræver opmærksomhed", - "settings.general.notifications.permissions.title": "Tilladelser", - "settings.general.notifications.permissions.description": "Vis systemmeddelelse når en tilladelse er påkrævet", - "settings.general.notifications.errors.title": "Fejl", - "settings.general.notifications.errors.description": "Vis systemmeddelelse når der opstår en fejl", - - "settings.general.sounds.agent.title": "Agent", - "settings.general.sounds.agent.description": "Afspil lyd når agenten er færdig eller kræver opmærksomhed", - "settings.general.sounds.permissions.title": "Tilladelser", - "settings.general.sounds.permissions.description": "Afspil lyd når en tilladelse er påkrævet", - "settings.general.sounds.errors.title": "Fejl", - "settings.general.sounds.errors.description": "Afspil lyd når der opstår en fejl", - - "settings.shortcuts.title": "Tastaturgenveje", - "settings.shortcuts.reset.button": "Nulstil til standard", - "settings.shortcuts.reset.toast.title": "Genveje nulstillet", - "settings.shortcuts.reset.toast.description": "Tastaturgenveje er blevet nulstillet til standard.", - "settings.shortcuts.conflict.title": "Genvej allerede i brug", - "settings.shortcuts.conflict.description": "{{keybind}} er allerede tildelt til {{titles}}.", - "settings.shortcuts.unassigned": "Ikke tildelt", - "settings.shortcuts.pressKeys": "Tryk på taster", - "settings.shortcuts.search.placeholder": "Søg genveje", - "settings.shortcuts.search.empty": "Ingen genveje fundet", - - "settings.shortcuts.group.general": "Generelt", - "settings.shortcuts.group.session": "Session", - "settings.shortcuts.group.navigation": "Navigation", - "settings.shortcuts.group.modelAndAgent": "Model og agent", - "settings.shortcuts.group.terminal": "Terminal", - "settings.shortcuts.group.prompt": "Prompt", - - "settings.providers.title": "Udbydere", - "settings.providers.description": "Udbyderindstillinger vil kunne konfigureres her.", - "settings.providers.section.connected": "Forbundne udbydere", - "settings.providers.connected.empty": "Ingen forbundne udbydere", - "settings.providers.section.popular": "Populære udbydere", - "settings.providers.tag.environment": "Miljø", - "settings.providers.tag.config": "Konfiguration", - "settings.providers.tag.custom": "Brugerdefineret", - "settings.providers.tag.other": "Andet", - "settings.models.title": "Modeller", - "settings.models.description": "Modelindstillinger vil kunne konfigureres her.", - "settings.agents.title": "Agenter", - "settings.agents.description": "Agentindstillinger vil kunne konfigureres her.", - "settings.commands.title": "Kommandoer", - "settings.commands.description": "Kommandoindstillinger vil kunne konfigureres her.", - "settings.mcp.title": "MCP", - "settings.mcp.description": "MCP-indstillinger vil kunne konfigureres her.", - - "settings.permissions.title": "Tilladelser", - "settings.permissions.description": "Styr hvilke værktøjer serveren kan bruge som standard.", - "settings.permissions.section.tools": "Værktøjer", - "settings.permissions.toast.updateFailed.title": "Kunne ikke opdatere tilladelser", - - "settings.permissions.action.allow": "Tillad", - "settings.permissions.action.ask": "Spørg", - "settings.permissions.action.deny": "Afvis", - - "settings.permissions.tool.read.title": "Læs", - "settings.permissions.tool.read.description": "Læsning af en fil (matcher filstien)", - "settings.permissions.tool.edit.title": "Rediger", - "settings.permissions.tool.edit.description": - "Ændre filer, herunder redigeringer, skrivninger, patches og multi-redigeringer", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "Match filer ved hjælp af glob-mønstre", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "Søg i filindhold ved hjælp af regulære udtryk", - "settings.permissions.tool.list.title": "Liste", - "settings.permissions.tool.list.description": "List filer i en mappe", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "Kør shell-kommandoer", - "settings.permissions.tool.task.title": "Opgave", - "settings.permissions.tool.task.description": "Start underagenter", - "settings.permissions.tool.skill.title": "Færdighed", - "settings.permissions.tool.skill.description": "Indlæs en færdighed efter navn", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "Kør sprogserverforespørgsler", - "settings.permissions.tool.todowrite.title": "Skriv To-do", - "settings.permissions.tool.todowrite.description": "Opdater to-do listen", - "settings.permissions.tool.webfetch.title": "Webhentning", - "settings.permissions.tool.webfetch.description": "Hent indhold fra en URL", - "settings.permissions.tool.websearch.title": "Websøgning", - "settings.permissions.tool.websearch.description": "Søg på nettet", - "settings.permissions.tool.codesearch.title": "Kodesøgning", - "settings.permissions.tool.codesearch.description": "Søg kode på nettet", - "settings.permissions.tool.external_directory.title": "Ekstern mappe", - "settings.permissions.tool.external_directory.description": "Få adgang til filer uden for projektmappen", - "settings.permissions.tool.doom_loop.title": "Doom Loop", - "settings.permissions.tool.doom_loop.description": "Opdag gentagne værktøjskald med identisk input", - - "session.delete.failed.title": "Kunne ikke slette session", - "session.delete.title": "Slet session", - "session.delete.confirm": 'Slet session "{{name}}"?', - "session.delete.button": "Slet session", - - "workspace.new": "Nyt arbejdsområde", - "workspace.type.local": "lokal", - "workspace.type.sandbox": "sandkasse", - "workspace.create.failed.title": "Kunne ikke oprette arbejdsområde", - "workspace.delete.failed.title": "Kunne ikke slette arbejdsområde", - "workspace.resetting.title": "Nulstiller arbejdsområde", - "workspace.resetting.description": "Dette kan tage et minut.", - "workspace.reset.failed.title": "Kunne ikke nulstille arbejdsområde", - "workspace.reset.success.title": "Arbejdsområde nulstillet", - "workspace.reset.success.description": "Arbejdsområdet matcher nu hovedgrenen.", - "workspace.error.stillPreparing": "Arbejdsområdet er stadig ved at blive klargjort", - "workspace.status.checking": "Tjekker for uflettede ændringer...", - "workspace.status.error": "Kunne ikke bekræfte git-status.", - "workspace.status.clean": "Ingen uflettede ændringer fundet.", - "workspace.status.dirty": "Uflettede ændringer fundet i dette arbejdsområde.", - "workspace.delete.title": "Slet arbejdsområde", - "workspace.delete.confirm": 'Slet arbejdsområde "{{name}}"?', - "workspace.delete.button": "Slet arbejdsområde", - "workspace.reset.title": "Nulstil arbejdsområde", - "workspace.reset.confirm": 'Nulstil arbejdsområde "{{name}}"?', - "workspace.reset.button": "Nulstil arbejdsområde", - "workspace.reset.archived.none": "Ingen aktive sessioner vil blive arkiveret.", - "workspace.reset.archived.one": "1 session vil blive arkiveret.", - "workspace.reset.archived.many": "{{count}} sessioner vil blive arkiveret.", - "workspace.reset.note": "Dette vil nulstille arbejdsområdet til at matche hovedgrenen.", - "common.open": "Åbn", - "dialog.releaseNotes.action.getStarted": "Kom i gang", - "dialog.releaseNotes.action.next": "Næste", - "dialog.releaseNotes.action.hideFuture": "Vis ikke disse i fremtiden", - "dialog.releaseNotes.media.alt": "Forhåndsvisning af udgivelse", - "toast.project.reloadFailed.title": "Kunne ikke genindlæse {{project}}", - "error.server.invalidConfiguration": "Ugyldig konfiguration", - "common.moreCountSuffix": " (+{{count}} mere)", - "common.time.justNow": "Lige nu", - "common.time.minutesAgo.short": "{{count}}m siden", - "common.time.hoursAgo.short": "{{count}}t siden", - "common.time.daysAgo.short": "{{count}}d siden", - "settings.providers.connected.environmentDescription": "Tilsluttet fra dine miljøvariabler", - "settings.providers.custom.description": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.", - - "app.server.unreachable": "Kunne ikke nå {{server}}", - "app.server.retrying": "Prøver igen automatisk...", - "app.server.otherServers": "Andre servere", - "dialog.server.add.usernamePlaceholder": "brugernavn", - "dialog.server.add.passwordPlaceholder": "adgangskode", - "server.row.noUsername": "intet brugernavn", - "session.review.noVcs.createGit.title": "Opret et Git-repository", - "session.review.noVcs.createGit.description": "Spor, gennemgå og fortryd ændringer i dette projekt", - "session.review.noVcs.createGit.actionLoading": "Opretter Git-repository...", - "session.review.noVcs.createGit.action": "Opret Git-repository", - "session.todo.progress": "{{done}} af {{total}} opgaver fuldført", - "session.question.progress": "{{current}} af {{total}} spørgsmål", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "Stifinder", - "session.header.open.fileManager": "Filhåndtering", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "Terminal", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "Udviklingsydelsesdiagnostik", - "debugBar.na": "n/a", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": - "Sidste gennemførte ruteovergang, der berører en sessionsside, målt fra routerstart til den første optegning efter den falder til ro.", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "Rullende billeder pr. sekund over de sidste 5 sekunder.", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "Værste billedtid over de sidste 5 sekunder.", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "Billeder over 32ms i de sidste 5 sekunder.", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "Blokeret tid og antal lange opgaver i de sidste 5 sekunder. Maks opgave: {{max}}.", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "Værste observerede inputforsinkelse i de sidste 5 sekunder.", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": - "Omtrentlig interaktionsvarighed over de sidste 5 sekunder. Dette er INP-lignende, ikke den officielle Web Vitals INP.", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "Kumulativt layoutskift for den nuværende app-levetid.", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "Brugt JS-heap vs heap-grænse. Kun Chromium.", - "debugBar.mem.tip": "Brugt JS-heap vs heap-grænse. {{used}} af {{limit}}.", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "Mellemrum", - "common.key.backspace": "Backspace", - "common.key.enter": "Enter", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "ukendt", - "error.page.circular": "[Cirkulær]", - "error.globalSDK.noServerAvailable": "Ingen server tilgængelig", - "error.globalSDK.serverNotAvailable": "Server ikke tilgængelig", - "error.childStore.persistedCacheCreateFailed": "Kunne ikke oprette vedvarende cache", - "error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke oprette vedvarende projektmetadata", - "error.childStore.persistedProjectIconCreateFailed": "Kunne ikke oprette vedvarende projektikon", - "error.childStore.storeCreateFailed": "Kunne ikke oprette lager", - "terminal.connectionLost.abnormalClose": "WebSocket lukkede unormalt: {{code}}", -} diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts deleted file mode 100644 index 34ce312aea2..00000000000 --- a/packages/app/src/i18n/de.ts +++ /dev/null @@ -1,870 +0,0 @@ -import { dict as en } from "./en" - -type Keys = keyof typeof en - -export const dict = { - "command.category.suggested": "Vorgeschlagen", - "command.category.view": "Ansicht", - "command.category.project": "Projekt", - "command.category.provider": "Anbieter", - "command.category.server": "Server", - "command.category.session": "Sitzung", - "command.category.theme": "Thema", - "command.category.language": "Sprache", - "command.category.file": "Datei", - "command.category.context": "Kontext", - "command.category.terminal": "Terminal", - "command.category.model": "Modell", - "command.category.mcp": "MCP", - "command.category.agent": "Agent", - "command.category.permissions": "Berechtigungen", - "command.category.workspace": "Arbeitsbereich", - "command.category.settings": "Einstellungen", - "theme.scheme.system": "System", - "theme.scheme.light": "Hell", - "theme.scheme.dark": "Dunkel", - "command.sidebar.toggle": "Seitenleiste umschalten", - "command.project.open": "Projekt öffnen", - "command.provider.connect": "Anbieter verbinden", - "command.server.switch": "Server wechseln", - "command.settings.open": "Einstellungen öffnen", - "command.session.previous": "Vorherige Sitzung", - "command.session.next": "Nächste Sitzung", - "command.session.previous.unseen": "Vorherige ungelesene Sitzung", - "command.session.next.unseen": "Nächste ungelesene Sitzung", - "command.session.archive": "Sitzung archivieren", - "command.palette": "Befehlspalette", - "command.theme.cycle": "Thema wechseln", - "command.theme.set": "Thema verwenden: {{theme}}", - "command.theme.scheme.cycle": "Farbschema wechseln", - "command.theme.scheme.set": "Farbschema verwenden: {{scheme}}", - "command.language.cycle": "Sprache wechseln", - "command.language.set": "Sprache verwenden: {{language}}", - "command.session.new": "Neue Sitzung", - "command.file.open": "Datei öffnen", - "command.tab.close": "Tab schließen", - "command.context.addSelection": "Auswahl zum Kontext hinzufügen", - "command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen", - "command.input.focus": "Eingabefeld fokussieren", - "command.terminal.toggle": "Terminal umschalten", - "command.fileTree.toggle": "Dateibaum umschalten", - "command.review.toggle": "Überprüfung umschalten", - "command.terminal.new": "Neues Terminal", - "command.terminal.new.description": "Neuen Terminal-Tab erstellen", - "command.steps.toggle": "Schritte umschalten", - "command.steps.toggle.description": "Schritte für die aktuelle Nachricht anzeigen oder ausblenden", - "command.message.previous": "Vorherige Nachricht", - "command.message.previous.description": "Zur vorherigen Benutzernachricht gehen", - "command.message.next": "Nächste Nachricht", - "command.message.next.description": "Zur nächsten Benutzernachricht gehen", - "command.model.choose": "Modell wählen", - "command.model.choose.description": "Ein anderes Modell auswählen", - "command.mcp.toggle": "MCPs umschalten", - "command.mcp.toggle.description": "MCPs umschalten", - "command.agent.cycle": "Agent wechseln", - "command.agent.cycle.description": "Zum nächsten Agenten wechseln", - "command.agent.cycle.reverse": "Agent rückwärts wechseln", - "command.agent.cycle.reverse.description": "Zum vorherigen Agenten wechseln", - "command.model.variant.cycle": "Denkaufwand wechseln", - "command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln", - "command.prompt.mode.shell": "Shell", - "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Berechtigungen automatisch akzeptieren", - "command.permissions.autoaccept.disable": "Automatische Akzeptanz von Berechtigungen stoppen", - "command.workspace.toggle": "Arbeitsbereiche umschalten", - "command.workspace.toggle.description": "Mehrere Arbeitsbereiche in der Seitenleiste aktivieren oder deaktivieren", - "command.session.undo": "Rückgängig", - "command.session.undo.description": "Letzte Nachricht rückgängig machen", - "command.session.redo": "Wiederherstellen", - "command.session.redo.description": "Letzte rückgängig gemachte Nachricht wiederherstellen", - "command.session.compact": "Sitzung komprimieren", - "command.session.compact.description": "Sitzung zusammenfassen, um die Kontextgröße zu reduzieren", - "command.session.fork": "Von Nachricht abzweigen", - "command.session.fork.description": "Neue Sitzung aus einer früheren Nachricht erstellen", - "command.session.share": "Sitzung teilen", - "command.session.share.description": "Diese Sitzung teilen und URL in die Zwischenablage kopieren", - "command.session.unshare": "Teilen der Sitzung aufheben", - "command.session.unshare.description": "Teilen dieser Sitzung beenden", - "palette.search.placeholder": "Dateien, Befehle und Sitzungen durchsuchen", - "palette.empty": "Keine Ergebnisse gefunden", - "palette.group.commands": "Befehle", - "palette.group.files": "Dateien", - "dialog.provider.search.placeholder": "Anbieter durchsuchen", - "dialog.provider.empty": "Keine Anbieter gefunden", - "dialog.provider.group.popular": "Beliebt", - "dialog.provider.group.other": "Andere", - "dialog.provider.tag.recommended": "Empfohlen", - "dialog.provider.opencode.note": "Kuratierte Modelle inklusive Claude, GPT, Gemini und mehr", - "dialog.provider.opencode.tagline": "Zuverlässige, optimierte Modelle", - "dialog.provider.opencodeGo.tagline": "Kostengünstiges Abo für alle", - "dialog.provider.anthropic.note": "Mit Claude Pro/Max oder API-Schlüssel verbinden", - "dialog.provider.copilot.note": "Mit Copilot oder API-Schlüssel verbinden", - "dialog.provider.openai.note": "Mit ChatGPT Pro/Plus oder API-Schlüssel verbinden", - "dialog.provider.google.note": "Gemini-Modelle für schnelle, strukturierte Antworten", - "dialog.provider.openrouter.note": "Zugriff auf alle unterstützten Modelle über einen Anbieter", - "dialog.provider.vercel.note": "Einheitlicher Zugriff auf KI-Modelle mit intelligentem Routing", - "dialog.model.select.title": "Modell auswählen", - "dialog.model.search.placeholder": "Modelle durchsuchen", - "dialog.model.empty": "Keine Modellergebnisse", - "dialog.model.manage": "Modelle verwalten", - "dialog.model.manage.description": "Anpassen, welche Modelle in der Modellauswahl erscheinen.", - "dialog.model.manage.provider.toggle": "Alle {{provider}}-Modelle umschalten", - "dialog.model.unpaid.freeModels.title": "Kostenlose Modelle von Kilo", - "dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen", - "dialog.provider.viewAll": "Mehr Anbieter anzeigen", - "provider.connect.title": "{{provider}} verbinden", - "provider.connect.title.anthropicProMax": "Mit Claude Pro/Max anmelden", - "provider.connect.selectMethod": "Anmeldemethode für {{provider}} auswählen.", - "provider.connect.method.apiKey": "API-Schlüssel", - "provider.connect.status.inProgress": "Autorisierung läuft...", - "provider.connect.status.waiting": "Warten auf Autorisierung...", - "provider.connect.status.failed": "Autorisierung fehlgeschlagen: {{error}}", - "provider.connect.apiKey.description": - "Geben Sie Ihren {{provider}} API-Schlüssel ein, um Ihr Konto zu verbinden und {{provider}} Modelle in Kilo zu nutzen.", - "provider.connect.apiKey.label": "{{provider}} API-Schlüssel", - "provider.connect.apiKey.placeholder": "API-Schlüssel", - "provider.connect.apiKey.required": "API-Schlüssel ist erforderlich", - "provider.connect.opencodeZen.line1": - "OpenCode Zen bietet Ihnen Zugriff auf eine kuratierte Auswahl zuverlässiger, optimierter Modelle für Coding-Agenten.", - "provider.connect.opencodeZen.line2": - "Mit einem einzigen API-Schlüssel erhalten Sie Zugriff auf Modelle wie Claude, GPT, Gemini, GLM und mehr.", - "provider.connect.opencodeZen.visit.prefix": "Besuchen Sie ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": ", um Ihren API-Schlüssel zu erhalten.", - "provider.connect.oauth.code.visit.prefix": "Besuchen Sie ", - "provider.connect.oauth.code.visit.link": "diesen Link", - "provider.connect.oauth.code.visit.suffix": - ", um Ihren Autorisierungscode zu erhalten, Ihr Konto zu verbinden und {{provider}} Modelle in Kilo zu nutzen.", - "provider.connect.oauth.code.label": "{{method}} Autorisierungscode", - "provider.connect.oauth.code.placeholder": "Autorisierungscode", - "provider.connect.oauth.code.required": "Autorisierungscode ist erforderlich", - "provider.connect.oauth.code.invalid": "Ungültiger Autorisierungscode", - "provider.connect.oauth.auto.visit.prefix": "Besuchen Sie ", - "provider.connect.oauth.auto.visit.link": "diesen Link", - "provider.connect.oauth.auto.visit.suffix": - " und geben Sie den untenstehenden Code ein, um Ihr Konto zu verbinden und {{provider}} Modelle in Kilo zu nutzen.", - "provider.connect.oauth.auto.confirmationCode": "Bestätigungscode", - "provider.connect.toast.connected.title": "{{provider}} verbunden", - "provider.connect.toast.connected.description": "{{provider}} Modelle sind jetzt verfügbar.", - "provider.custom.title": "Benutzerdefinierter Anbieter", - "provider.custom.description.prefix": "Konfigurieren Sie einen OpenAI-kompatiblen Anbieter. Siehe die ", - "provider.custom.description.link": "Anbieter-Konfigurationsdokumente", - "provider.custom.description.suffix": ".", - "provider.custom.field.providerID.label": "Anbieter-ID", - "provider.custom.field.providerID.placeholder": "myprovider", - "provider.custom.field.providerID.description": "Kleinbuchstaben, Zahlen, Bindestriche oder Unterstriche", - "provider.custom.field.name.label": "Anzeigename", - "provider.custom.field.name.placeholder": "Mein KI-Anbieter", - "provider.custom.field.baseURL.label": "Basis-URL", - "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", - "provider.custom.field.apiKey.label": "API-Schlüssel", - "provider.custom.field.apiKey.placeholder": "API-Schlüssel", - "provider.custom.field.apiKey.description": - "Optional. Leer lassen, wenn Sie die Authentifizierung über Header verwalten.", - "provider.custom.models.label": "Modelle", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "model-id", - "provider.custom.models.name.label": "Name", - "provider.custom.models.name.placeholder": "Anzeigename", - "provider.custom.models.remove": "Modell entfernen", - "provider.custom.models.add": "Modell hinzufügen", - "provider.custom.headers.label": "Header (optional)", - "provider.custom.headers.key.label": "Header", - "provider.custom.headers.key.placeholder": "Header-Name", - "provider.custom.headers.value.label": "Wert", - "provider.custom.headers.value.placeholder": "wert", - "provider.custom.headers.remove": "Header entfernen", - "provider.custom.headers.add": "Header hinzufügen", - "provider.custom.error.providerID.required": "Anbieter-ID ist erforderlich", - "provider.custom.error.providerID.format": "Verwenden Sie Kleinbuchstaben, Zahlen, Bindestriche oder Unterstriche", - "provider.custom.error.providerID.exists": "Diese Anbieter-ID existiert bereits", - "provider.custom.error.name.required": "Anzeigename ist erforderlich", - "provider.custom.error.baseURL.required": "Basis-URL ist erforderlich", - "provider.custom.error.baseURL.format": "Muss mit http:// oder https:// beginnen", - "provider.custom.error.required": "Erforderlich", - "provider.custom.error.duplicate": "Duplikat", - "provider.disconnect.toast.disconnected.title": "{{provider}} getrennt", - "provider.disconnect.toast.disconnected.description": "Die {{provider}}-Modelle sind nicht mehr verfügbar.", - "model.tag.free": "Kostenlos", - "model.tag.latest": "Neueste", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "Text", - "model.input.image": "Bild", - "model.input.audio": "Audio", - "model.input.video": "Video", - "model.input.pdf": "PDF", - "model.tooltip.allows": "Erlaubt: {{inputs}}", - "model.tooltip.reasoning.allowed": "Erlaubt Reasoning", - "model.tooltip.reasoning.none": "Kein Reasoning", - "model.tooltip.context": "Kontextlimit {{limit}}", - "common.search.placeholder": "Suchen", - "common.goBack": "Zurück", - "common.goForward": "Vorwärts navigieren", - "common.loading": "Laden", - "common.loading.ellipsis": "...", - "common.cancel": "Abbrechen", - "common.connect": "Verbinden", - "common.disconnect": "Trennen", - "common.continue": "Absenden", - "common.submit": "Absenden", - "common.save": "Speichern", - "common.saving": "Speichert...", - "common.default": "Standard", - "common.attachment": "Anhang", - "prompt.placeholder.shell": "Shell-Befehl eingeben... {{example}}", - "prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"', - "prompt.placeholder.simple": "Fragen Sie alles...", - "prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…", - "prompt.placeholder.summarizeComment": "Kommentar zusammenfassen…", - "prompt.mode.shell": "Shell", - "prompt.mode.normal": "Prompt", - "prompt.mode.shell.exit": "esc zum Verlassen", - "prompt.example.1": "Ein TODO in der Codebasis beheben", - "prompt.example.2": "Was ist der Tech-Stack dieses Projekts?", - "prompt.example.3": "Fehlerhafte Tests beheben", - "prompt.example.4": "Erkläre, wie die Authentifizierung funktioniert", - "prompt.example.5": "Sicherheitslücken finden und beheben", - "prompt.example.6": "Unit-Tests für den Benutzerdienst hinzufügen", - "prompt.example.7": "Diese Funktion lesbarer gestalten", - "prompt.example.8": "Was bedeutet dieser Fehler?", - "prompt.example.9": "Hilf mir, dieses Problem zu debuggen", - "prompt.example.10": "API-Dokumentation generieren", - "prompt.example.11": "Datenbankabfragen optimieren", - "prompt.example.12": "Eingabevalidierung hinzufügen", - "prompt.example.13": "Neue Komponente erstellen für...", - "prompt.example.14": "Wie deploye ich dieses Projekt?", - "prompt.example.15": "Meinen Code auf Best Practices überprüfen", - "prompt.example.16": "Fehlerbehandlung zu dieser Funktion hinzufügen", - "prompt.example.17": "Erkläre dieses Regex-Muster", - "prompt.example.18": "Dies in TypeScript konvertieren", - "prompt.example.19": "Logging in der gesamten Codebasis hinzufügen", - "prompt.example.20": "Welche Abhängigkeiten sind veraltet?", - "prompt.example.21": "Hilf mir, ein Migrationsskript zu schreiben", - "prompt.example.22": "Caching für diesen Endpunkt implementieren", - "prompt.example.23": "Paginierung zu dieser Liste hinzufügen", - "prompt.example.24": "CLI-Befehl erstellen für...", - "prompt.example.25": "Wie funktionieren Umgebungsvariablen hier?", - "prompt.popover.emptyResults": "Keine passenden Ergebnisse", - "prompt.popover.emptyCommands": "Keine passenden Befehle", - "prompt.dropzone.label": "Bilder, PDFs oder Textdateien hier ablegen", - "prompt.dropzone.file.label": "Ablegen zum @Erwähnen der Datei", - "prompt.slash.badge.custom": "benutzerdefiniert", - "prompt.slash.badge.skill": "Skill", - "prompt.slash.badge.mcp": "MCP", - "prompt.context.active": "aktiv", - "prompt.context.includeActiveFile": "Aktive Datei einbeziehen", - "prompt.context.removeActiveFile": "Aktive Datei aus dem Kontext entfernen", - "prompt.context.removeFile": "Datei aus dem Kontext entfernen", - "prompt.action.attachFile": "Datei anhängen", - "prompt.attachment.remove": "Anhang entfernen", - "prompt.action.send": "Senden", - "prompt.action.stop": "Stopp", - "prompt.toast.pasteUnsupported.title": "Nicht unterstützter Anhang", - "prompt.toast.pasteUnsupported.description": "Hier können nur Bilder, PDFs oder Textdateien angehängt werden.", - "prompt.toast.modelAgentRequired.title": "Wählen Sie einen Agenten und ein Modell", - "prompt.toast.modelAgentRequired.description": - "Wählen Sie einen Agenten und ein Modell, bevor Sie eine Eingabe senden.", - "prompt.toast.worktreeCreateFailed.title": "Worktree konnte nicht erstellt werden", - "prompt.toast.sessionCreateFailed.title": "Sitzung konnte nicht erstellt werden", - "prompt.toast.shellSendFailed.title": "Shell-Befehl konnte nicht gesendet werden", - "prompt.toast.commandSendFailed.title": "Befehl konnte nicht gesendet werden", - "prompt.toast.promptSendFailed.title": "Eingabe konnte nicht gesendet werden", - "prompt.toast.promptSendFailed.description": "Sitzung konnte nicht abgerufen werden", - "dialog.mcp.title": "MCPs", - "dialog.mcp.description": "{{enabled}} von {{total}} aktiviert", - "dialog.mcp.empty": "Keine MCPs konfiguriert", - "dialog.lsp.empty": "LSPs automatisch nach Dateityp erkannt", - "dialog.plugins.empty": "In opencode.json konfigurierte Plugins", - "mcp.status.connected": "verbunden", - "mcp.status.failed": "fehlgeschlagen", - "mcp.status.needs_auth": "benötigt Authentifizierung", - "mcp.status.disabled": "deaktiviert", - "dialog.fork.empty": "Keine Nachrichten zum Abzweigen vorhanden", - "dialog.directory.search.placeholder": "Ordner durchsuchen", - "dialog.directory.empty": "Keine Ordner gefunden", - "dialog.server.title": "Server", - "dialog.server.description": "Wechseln Sie den Kilo-Server, mit dem sich diese App verbindet.", - "dialog.server.search.placeholder": "Server durchsuchen", - "dialog.server.empty": "Noch keine Server", - "dialog.server.add.title": "Server hinzufügen", - "dialog.server.add.url": "Server-URL", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "Verbindung zum Server fehlgeschlagen", - "dialog.server.add.checking": "Prüfen...", - "dialog.server.add.button": "Server hinzufügen", - "dialog.server.add.name": "Servername (optional)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "Benutzername (optional)", - "dialog.server.add.password": "Passwort (optional)", - "dialog.server.edit.title": "Server bearbeiten", - "dialog.server.default.title": "Standardserver", - "dialog.server.default.description": - "Beim App-Start mit diesem Server verbinden, anstatt einen lokalen Server zu starten. Erfordert Neustart.", - "dialog.server.default.none": "Kein Server ausgewählt", - "dialog.server.default.set": "Aktuellen Server als Standard setzen", - "dialog.server.default.clear": "Löschen", - "dialog.server.action.remove": "Server entfernen", - "dialog.server.menu.edit": "Bearbeiten", - "dialog.server.menu.default": "Als Standard festlegen", - "dialog.server.menu.defaultRemove": "Standard entfernen", - "dialog.server.menu.delete": "Löschen", - "dialog.server.current": "Aktueller Server", - "dialog.server.status.default": "Standard", - "dialog.project.edit.title": "Projekt bearbeiten", - "dialog.project.edit.name": "Name", - "dialog.project.edit.icon": "Icon", - "dialog.project.edit.icon.alt": "Projekt-Icon", - "dialog.project.edit.icon.hint": "Klicken oder Bild ziehen", - "dialog.project.edit.icon.recommended": "Empfohlen: 128x128px", - "dialog.project.edit.color": "Farbe", - "dialog.project.edit.color.select": "{{color}}-Farbe auswählen", - "dialog.project.edit.worktree.startup": "Startup-Skript für Arbeitsbereich", - "dialog.project.edit.worktree.startup.description": - "Wird nach dem Erstellen eines neuen Arbeitsbereichs (Worktree) ausgeführt.", - "dialog.project.edit.worktree.startup.placeholder": "z. B. bun install", - "context.breakdown.title": "Kontext-Aufschlüsselung", - "context.breakdown.note": - 'Ungefähre Aufschlüsselung der Eingabe-Token. "Andere" beinhaltet Werkzeugdefinitionen und Overhead.', - "context.breakdown.system": "System", - "context.breakdown.user": "Benutzer", - "context.breakdown.assistant": "Assistent", - "context.breakdown.tool": "Werkzeugaufrufe", - "context.breakdown.other": "Andere", - "context.systemPrompt.title": "System-Prompt", - "context.rawMessages.title": "Rohdaten der Nachrichten", - "context.stats.session": "Sitzung", - "context.stats.messages": "Nachrichten", - "context.stats.provider": "Anbieter", - "context.stats.model": "Modell", - "context.stats.limit": "Kontextlimit", - "context.stats.totalTokens": "Gesamt-Token", - "context.stats.usage": "Nutzung", - "context.stats.inputTokens": "Eingabe-Token", - "context.stats.outputTokens": "Ausgabe-Token", - "context.stats.reasoningTokens": "Reasoning-Token", - "context.stats.cacheTokens": "Cache-Token (lesen/schreiben)", - "context.stats.userMessages": "Benutzernachrichten", - "context.stats.assistantMessages": "Assistentennachrichten", - "context.stats.totalCost": "Gesamtkosten", - "context.stats.sessionCreated": "Sitzung erstellt", - "context.stats.lastActivity": "Letzte Aktivität", - "context.usage.tokens": "Token", - "context.usage.usage": "Nutzung", - "context.usage.cost": "Kosten", - "context.usage.clickToView": "Klicken, um Kontext anzuzeigen", - "context.usage.view": "Kontextnutzung anzeigen", - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - "toast.language.title": "Sprache", - "toast.language.description": "Zu {{language}} gewechselt", - "toast.theme.title": "Thema gewechselt", - "toast.scheme.title": "Farbschema", - "toast.workspace.enabled.title": "Arbeitsbereiche aktiviert", - "toast.workspace.enabled.description": "Mehrere Worktrees werden jetzt in der Seitenleiste angezeigt", - "toast.workspace.disabled.title": "Arbeitsbereiche deaktiviert", - "toast.workspace.disabled.description": "Nur der Haupt-Worktree wird in der Seitenleiste angezeigt", - "toast.permissions.autoaccept.on.title": "Berechtigungen werden automatisch akzeptiert", - "toast.permissions.autoaccept.on.description": "Berechtigungsanfragen werden automatisch genehmigt", - "toast.permissions.autoaccept.off.title": "Automatische Akzeptanz von Berechtigungen gestoppt", - "toast.permissions.autoaccept.off.description": "Berechtigungsanfragen erfordern eine Genehmigung", - "toast.model.none.title": "Kein Modell ausgewählt", - "toast.model.none.description": "Verbinden Sie einen Anbieter, um diese Sitzung zusammenzufassen", - "toast.file.loadFailed.title": "Datei konnte nicht geladen werden", - "toast.file.listFailed.title": "Dateien konnten nicht aufgelistet werden", - "toast.context.noLineSelection.title": "Keine Zeilenauswahl", - "toast.context.noLineSelection.description": "Wählen Sie zuerst einen Zeilenbereich in einem Datei-Tab aus.", - "toast.session.share.copyFailed.title": "URL konnte nicht in die Zwischenablage kopiert werden", - "toast.session.share.success.title": "Sitzung geteilt", - "toast.session.share.success.description": "Teilen-URL in die Zwischenablage kopiert!", - "toast.session.share.failed.title": "Sitzung konnte nicht geteilt werden", - "toast.session.share.failed.description": "Beim Teilen der Sitzung ist ein Fehler aufgetreten", - "toast.session.unshare.success.title": "Teilen der Sitzung aufgehoben", - "toast.session.unshare.success.description": "Teilen der Sitzung erfolgreich aufgehoben!", - "toast.session.unshare.failed.title": "Aufheben des Teilens fehlgeschlagen", - "toast.session.unshare.failed.description": "Beim Aufheben des Teilens ist ein Fehler aufgetreten", - "toast.session.listFailed.title": "Sitzungen für {{project}} konnten nicht geladen werden", - "toast.update.title": "Update verfügbar", - "toast.update.description": "Eine neue Version von Kilo ({{version}}) ist zur Installation verfügbar.", - "toast.update.action.installRestart": "Installieren und neu starten", - "toast.update.action.notYet": "Noch nicht", - "error.page.title": "Etwas ist schiefgelaufen", - "error.page.description": "Beim Laden der Anwendung ist ein Fehler aufgetreten.", - "error.page.details.label": "Fehlerdetails", - "error.page.action.restart": "Neustart", - "error.page.action.checking": "Prüfen...", - "error.page.action.checkUpdates": "Nach Updates suchen", - "error.page.action.updateTo": "Auf {{version}} aktualisieren", - "error.page.report.prefix": "Bitte melden Sie diesen Fehler dem Kilo-Team", - "error.page.report.discord": "auf Discord", - "error.page.version": "Version: {{version}}", - "error.dev.rootNotFound": - "Wurzelelement nicht gefunden. Haben Sie vergessen, es in Ihre index.html aufzunehmen? Oder wurde das id-Attribut falsch geschrieben?", - "error.globalSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?", - "directory.error.invalidUrl": "Ungültiges Verzeichnis in der URL.", - "error.chain.unknown": "Unbekannter Fehler", - "error.chain.causedBy": "Verursacht durch:", - "error.chain.apiError": "API-Fehler", - "error.chain.status": "Status: {{status}}", - "error.chain.retryable": "Wiederholbar: {{retryable}}", - "error.chain.responseBody": "Antwort-Body:\n{{body}}", - "error.chain.didYouMean": "Meinten Sie: {{suggestions}}", - "error.chain.modelNotFound": "Modell nicht gefunden: {{provider}}/{{model}}", - "error.chain.checkConfig": "Überprüfen Sie Ihre Konfiguration (opencode.json) auf Anbieter-/Modellnamen", - "error.chain.mcpFailed": - 'MCP-Server "{{name}}" fehlgeschlagen. Hinweis: Kilo unterstützt noch keine MCP-Authentifizierung.', - "error.chain.providerAuthFailed": "Anbieter-Authentifizierung fehlgeschlagen ({{provider}}): {{message}}", - "error.chain.providerInitFailed": - 'Anbieter "{{provider}}" konnte nicht initialisiert werden. Überprüfen Sie Anmeldeinformationen und Konfiguration.', - "error.chain.configJsonInvalid": "Konfigurationsdatei unter {{path}} ist kein gültiges JSON(C)", - "error.chain.configJsonInvalidWithMessage": - "Konfigurationsdatei unter {{path}} ist kein gültiges JSON(C): {{message}}", - "error.chain.configDirectoryTypo": - 'Verzeichnis "{{dir}}" in {{path}} ist ungültig. Benennen Sie das Verzeichnis in "{{suggestion}}" um oder entfernen Sie es. Dies ist ein häufiger Tippfehler.', - "error.chain.configFrontmatterError": "Frontmatter in {{path}} konnte nicht geparst werden:\n{{message}}", - "error.chain.configInvalid": "Konfigurationsdatei unter {{path}} ist ungültig", - "error.chain.configInvalidWithMessage": "Konfigurationsdatei unter {{path}} ist ungültig: {{message}}", - "notification.permission.title": "Berechtigung erforderlich", - "notification.permission.description": "{{sessionTitle}} in {{projectName}} benötigt Berechtigung", - "notification.question.title": "Frage", - "notification.question.description": "{{sessionTitle}} in {{projectName}} hat eine Frage", - "notification.action.goToSession": "Zur Sitzung gehen", - "notification.session.responseReady.title": "Antwort bereit", - "notification.session.error.title": "Sitzungsfehler", - "notification.session.error.fallbackDescription": "Ein Fehler ist aufgetreten", - "home.recentProjects": "Letzte Projekte", - "home.empty.title": "Keine letzten Projekte", - "home.empty.description": "Starten Sie, indem Sie ein lokales Projekt öffnen", - "session.tab.session": "Sitzung", - "session.tab.review": "Überprüfung", - "session.tab.context": "Kontext", - "session.panel.reviewAndFiles": "Überprüfung und Dateien", - "session.review.filesChanged": "{{count}} Dateien geändert", - "session.review.change.one": "Änderung", - "session.review.change.other": "Änderungen", - "session.review.loadingChanges": "Lade Änderungen...", - "session.review.empty": "Noch keine Änderungen in dieser Sitzung", - "session.review.noVcs": "Kein Git-Versionskontrollsystem erkannt, Änderungen werden nicht angezeigt", - "session.review.noSnapshot": - "Snapshot-Tracking ist in der Konfiguration deaktiviert, daher sind Sitzungsänderungen nicht verfügbar", - "session.review.noChanges": "Keine Änderungen", - "session.files.selectToOpen": "Datei zum Öffnen auswählen", - "session.files.all": "Alle Dateien", - "session.files.empty": "Keine Dateien", - "session.files.binaryContent": "Binärdatei (Inhalt kann nicht angezeigt werden)", - "session.messages.renderEarlier": "Frühere Nachrichten rendern", - "session.messages.loadingEarlier": "Lade frühere Nachrichten...", - "session.messages.loadEarlier": "Frühere Nachrichten laden", - "session.messages.loading": "Lade Nachrichten...", - "session.messages.jumpToLatest": "Zum neuesten springen", - "session.context.addToContext": "{{selection}} zum Kontext hinzufügen", - "session.todo.title": "Aufgaben", - "session.todo.collapse": "Einklappen", - "session.todo.expand": "Ausklappen", - "session.followupDock.summary.one": "{{count}} Nachricht in der Warteschlange", - "session.followupDock.summary.other": "{{count}} Nachrichten in der Warteschlange", - "session.followupDock.sendNow": "Jetzt senden", - "session.followupDock.edit": "Bearbeiten", - "session.followupDock.collapse": "Warteschlange einklappen", - "session.followupDock.expand": "Warteschlange ausklappen", - "session.revertDock.summary.one": "{{count}} zurückgesetzte Nachricht", - "session.revertDock.summary.other": "{{count}} zurückgesetzte Nachrichten", - "session.revertDock.collapse": "Zurückgesetzte Nachrichten einklappen", - "session.revertDock.expand": "Zurückgesetzte Nachrichten ausklappen", - "session.revertDock.restore": "Nachricht wiederherstellen", - "session.new.title": "Baue, was du willst", - "session.new.worktree.main": "Haupt-Branch", - "session.new.worktree.mainWithBranch": "Haupt-Branch ({{branch}})", - "session.new.worktree.create": "Neuen Worktree erstellen", - "session.new.lastModified": "Zuletzt geändert", - "session.header.search.placeholder": "{{project}} durchsuchen", - "session.header.searchFiles": "Dateien suchen", - "session.header.openIn": "Öffnen in", - "session.header.open.action": "{{app}} öffnen", - "session.header.open.ariaLabel": "In {{app}} öffnen", - "session.header.open.menu": "Öffnen-Optionen", - "session.header.open.copyPath": "Pfad kopieren", - "status.popover.trigger": "Status", - "status.popover.ariaLabel": "Serverkonfigurationen", - "status.popover.tab.servers": "Server", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "Plugins", - "status.popover.action.manageServers": "Server verwalten", - "session.share.popover.title": "Im Web veröffentlichen", - "session.share.popover.description.shared": - "Diese Sitzung ist öffentlich im Web. Sie ist für jeden mit dem Link zugänglich.", - "session.share.popover.description.unshared": - "Sitzung öffentlich im Web teilen. Sie wird für jeden mit dem Link zugänglich sein.", - "session.share.action.share": "Teilen", - "session.share.action.publish": "Veröffentlichen", - "session.share.action.publishing": "Veröffentliche...", - "session.share.action.unpublish": "Veröffentlichung aufheben", - "session.share.action.unpublishing": "Hebe Veröffentlichung auf...", - "session.share.action.view": "Ansehen", - "session.share.copy.copied": "Kopiert", - "session.share.copy.copyLink": "Link kopieren", - "lsp.tooltip.none": "Keine LSP-Server", - "lsp.label.connected": "{{count}} LSP", - "prompt.loading": "Lade Prompt...", - "terminal.loading": "Lade Terminal...", - "terminal.title": "Terminal", - "terminal.title.numbered": "Terminal {{number}}", - "terminal.close": "Terminal schließen", - "terminal.connectionLost.title": "Verbindung verloren", - "terminal.connectionLost.description": - "Die Terminalverbindung wurde unterbrochen. Das kann passieren, wenn der Server neu startet.", - "common.closeTab": "Tab schließen", - "common.dismiss": "Verwerfen", - "common.requestFailed": "Anfrage fehlgeschlagen", - "common.moreOptions": "Weitere Optionen", - "common.learnMore": "Mehr erfahren", - "common.rename": "Umbenennen", - "common.reset": "Zurücksetzen", - "common.archive": "Archivieren", - "common.delete": "Löschen", - "common.close": "Schließen", - "common.edit": "Bearbeiten", - "common.loadMore": "Mehr laden", - "common.key.esc": "ESC", - "sidebar.menu.toggle": "Menü umschalten", - "sidebar.nav.projectsAndSessions": "Projekte und Sitzungen", - "sidebar.settings": "Einstellungen", - "sidebar.help": "Hilfe", - "sidebar.workspaces.enable": "Arbeitsbereiche aktivieren", - "sidebar.workspaces.disable": "Arbeitsbereiche deaktivieren", - "sidebar.gettingStarted.title": "Erste Schritte", - "sidebar.gettingStarted.line1": "Kilo enthält kostenlose Modelle, damit Sie sofort loslegen können.", - "sidebar.gettingStarted.line2": - "Verbinden Sie einen beliebigen Anbieter, um Modelle wie Claude, GPT, Gemini usw. zu nutzen.", - "sidebar.project.recentSessions": "Letzte Sitzungen", - "sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen", - "sidebar.project.clearNotifications": "Benachrichtigungen löschen", - "app.name.desktop": "Kilo Desktop", - "settings.section.desktop": "Desktop", - "settings.section.server": "Server", - "settings.tab.general": "Allgemein", - "settings.tab.shortcuts": "Tastenkombinationen", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL-Integration", - "settings.desktop.wsl.description": "Kilo-Server innerhalb von WSL unter Windows ausführen.", - "settings.general.section.appearance": "Erscheinungsbild", - "settings.general.section.notifications": "Systembenachrichtigungen", - "settings.general.section.updates": "Updates", - "settings.general.section.sounds": "Soundeffekte", - "settings.general.section.feed": "Feed", - "settings.general.section.display": "Anzeige", - "settings.general.row.language.title": "Sprache", - "settings.general.row.language.description": "Die Anzeigesprache für Kilo ändern", - "settings.general.row.appearance.title": "Erscheinungsbild", - "settings.general.row.appearance.description": "Anpassen, wie Kilo auf Ihrem Gerät aussieht", - "settings.general.row.colorScheme.title": "Farbschema", - "settings.general.row.colorScheme.description": "Wählen Sie, ob Kilo dem System-, hellen oder dunklen Thema folgt", - "settings.general.row.theme.title": "Thema", - "settings.general.row.theme.description": "Das Thema von Kilo anpassen.", - "settings.general.row.font.title": "Code-Schriftart", - "settings.general.row.font.description": "Die in Codeblöcken verwendete Schriftart anpassen", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "UI-Schriftart", - "settings.general.row.uiFont.description": "Die im gesamten Interface verwendete Schriftart anpassen", - "settings.general.row.followup.title": "Verhalten bei Folgefragen", - "settings.general.row.followup.description": - "Wählen Sie, ob Folgefragen sofort steuern oder in einer Warteschlange warten", - "settings.general.row.followup.option.queue": "Warteschlange", - "settings.general.row.followup.option.steer": "Steuern", - "settings.general.row.reasoningSummaries.title": "Reasoning-Zusammenfassungen anzeigen", - "settings.general.row.reasoningSummaries.description": - "Zusammenfassungen des Modell-Reasonings in der Timeline anzeigen", - "settings.general.row.shellToolPartsExpanded.title": "Shell-Tool-Abschnitte ausklappen", - "settings.general.row.shellToolPartsExpanded.description": - "Shell-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen", - "settings.general.row.editToolPartsExpanded.title": "Edit-Tool-Abschnitte ausklappen", - "settings.general.row.editToolPartsExpanded.description": - "Edit-, Write- und Patch-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen", - "settings.general.row.showSessionProgressBar.title": "Sitzungsfortschrittsleiste anzeigen", - "settings.general.row.showSessionProgressBar.description": - "Die animierte Fortschrittsleiste oben in der Sitzung anzeigen, wenn der Agent arbeitet", - "settings.general.row.wayland.title": "Natives Wayland verwenden", - "settings.general.row.wayland.description": "X11-Fallback unter Wayland deaktivieren. Erfordert Neustart.", - "settings.general.row.wayland.tooltip": - "Unter Linux mit Monitoren unterschiedlicher Bildwiederholraten kann natives Wayland stabiler sein.", - "settings.general.row.releaseNotes.title": "Versionshinweise", - "settings.general.row.releaseNotes.description": '"Neuigkeiten"-Pop-ups nach Updates anzeigen', - "settings.updates.row.startup.title": "Beim Start nach Updates suchen", - "settings.updates.row.startup.description": "Beim Start von Kilo automatisch nach Updates suchen", - "settings.updates.row.check.title": "Nach Updates suchen", - "settings.updates.row.check.description": "Manuell nach Updates suchen und installieren, wenn verfügbar", - "settings.updates.action.checkNow": "Jetzt prüfen", - "settings.updates.action.checking": "Wird geprüft...", - "settings.updates.toast.latest.title": "Du bist auf dem neuesten Stand", - "settings.updates.toast.latest.description": "Du verwendest die aktuelle Version von Kilo.", - "sound.option.none": "Keine", - "sound.option.alert01": "Alarm 01", - "sound.option.alert02": "Alarm 02", - "sound.option.alert03": "Alarm 03", - "sound.option.alert04": "Alarm 04", - "sound.option.alert05": "Alarm 05", - "sound.option.alert06": "Alarm 06", - "sound.option.alert07": "Alarm 07", - "sound.option.alert08": "Alarm 08", - "sound.option.alert09": "Alarm 09", - "sound.option.alert10": "Alarm 10", - "sound.option.bipbop01": "Bip-bop 01", - "sound.option.bipbop02": "Bip-bop 02", - "sound.option.bipbop03": "Bip-bop 03", - "sound.option.bipbop04": "Bip-bop 04", - "sound.option.bipbop05": "Bip-bop 05", - "sound.option.bipbop06": "Bip-bop 06", - "sound.option.bipbop07": "Bip-bop 07", - "sound.option.bipbop08": "Bip-bop 08", - "sound.option.bipbop09": "Bip-bop 09", - "sound.option.bipbop10": "Bip-bop 10", - "sound.option.staplebops01": "Staplebops 01", - "sound.option.staplebops02": "Staplebops 02", - "sound.option.staplebops03": "Staplebops 03", - "sound.option.staplebops04": "Staplebops 04", - "sound.option.staplebops05": "Staplebops 05", - "sound.option.staplebops06": "Staplebops 06", - "sound.option.staplebops07": "Staplebops 07", - "sound.option.nope01": "Nein 01", - "sound.option.nope02": "Nein 02", - "sound.option.nope03": "Nein 03", - "sound.option.nope04": "Nein 04", - "sound.option.nope05": "Nein 05", - "sound.option.nope06": "Nein 06", - "sound.option.nope07": "Nein 07", - "sound.option.nope08": "Nein 08", - "sound.option.nope09": "Nein 09", - "sound.option.nope10": "Nein 10", - "sound.option.nope11": "Nein 11", - "sound.option.nope12": "Nein 12", - "sound.option.yup01": "Ja 01", - "sound.option.yup02": "Ja 02", - "sound.option.yup03": "Ja 03", - "sound.option.yup04": "Ja 04", - "sound.option.yup05": "Ja 05", - "sound.option.yup06": "Ja 06", - "settings.general.notifications.agent.title": "Agent", - "settings.general.notifications.agent.description": - "Systembenachrichtigung anzeigen, wenn der Agent fertig ist oder Aufmerksamkeit benötigt", - "settings.general.notifications.permissions.title": "Berechtigungen", - "settings.general.notifications.permissions.description": - "Systembenachrichtigung anzeigen, wenn eine Berechtigung erforderlich ist", - "settings.general.notifications.errors.title": "Fehler", - "settings.general.notifications.errors.description": "Systembenachrichtigung anzeigen, wenn ein Fehler auftritt", - "settings.general.sounds.agent.title": "Agent", - "settings.general.sounds.agent.description": "Ton abspielen, wenn der Agent fertig ist oder Aufmerksamkeit benötigt", - "settings.general.sounds.permissions.title": "Berechtigungen", - "settings.general.sounds.permissions.description": "Ton abspielen, wenn eine Berechtigung erforderlich ist", - "settings.general.sounds.errors.title": "Fehler", - "settings.general.sounds.errors.description": "Ton abspielen, wenn ein Fehler auftritt", - "settings.shortcuts.title": "Tastenkombinationen", - "settings.shortcuts.reset.button": "Auf Standard zurücksetzen", - "settings.shortcuts.reset.toast.title": "Tastenkombinationen zurückgesetzt", - "settings.shortcuts.reset.toast.description": "Die Tastenkombinationen wurden auf die Standardwerte zurückgesetzt.", - "settings.shortcuts.conflict.title": "Tastenkombination bereits in Verwendung", - "settings.shortcuts.conflict.description": "{{keybind}} ist bereits {{titles}} zugewiesen.", - "settings.shortcuts.unassigned": "Nicht zugewiesen", - "settings.shortcuts.pressKeys": "Tasten drücken", - "settings.shortcuts.search.placeholder": "Tastenkürzel suchen", - "settings.shortcuts.search.empty": "Keine Tastenkürzel gefunden", - "settings.shortcuts.group.general": "Allgemein", - "settings.shortcuts.group.session": "Sitzung", - "settings.shortcuts.group.navigation": "Navigation", - "settings.shortcuts.group.modelAndAgent": "Modell und Agent", - "settings.shortcuts.group.terminal": "Terminal", - "settings.shortcuts.group.prompt": "Prompt", - "settings.providers.title": "Anbieter", - "settings.providers.description": "Anbietereinstellungen können hier konfiguriert werden.", - "settings.providers.section.connected": "Verbundene Anbieter", - "settings.providers.connected.empty": "Keine verbundenen Anbieter", - "settings.providers.section.popular": "Beliebte Anbieter", - "settings.providers.tag.environment": "Umgebung", - "settings.providers.tag.config": "Konfiguration", - "settings.providers.tag.custom": "Benutzerdefiniert", - "settings.providers.tag.other": "Andere", - "settings.models.title": "Modelle", - "settings.models.description": "Modelleinstellungen können hier konfiguriert werden.", - "settings.agents.title": "Agenten", - "settings.agents.description": "Agenteneinstellungen können hier konfiguriert werden.", - "settings.commands.title": "Befehle", - "settings.commands.description": "Befehlseinstellungen können hier konfiguriert werden.", - "settings.mcp.title": "MCP", - "settings.mcp.description": "MCP-Einstellungen können hier konfiguriert werden.", - "settings.permissions.title": "Berechtigungen", - "settings.permissions.description": "Steuern Sie, welche Tools der Server standardmäßig verwenden darf.", - "settings.permissions.section.tools": "Tools", - "settings.permissions.toast.updateFailed.title": "Berechtigungen konnten nicht aktualisiert werden", - "settings.permissions.action.allow": "Erlauben", - "settings.permissions.action.ask": "Fragen", - "settings.permissions.action.deny": "Verweigern", - "settings.permissions.tool.read.title": "Lesen", - "settings.permissions.tool.read.description": "Lesen einer Datei (stimmt mit dem Dateipfad überein)", - "settings.permissions.tool.edit.title": "Bearbeiten", - "settings.permissions.tool.edit.description": - "Dateien ändern, einschließlich Bearbeitungen, Schreibvorgängen, Patches und Mehrfachbearbeitungen", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "Dateien mithilfe von Glob-Mustern abgleichen", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "Dateiinhalte mit regulären Ausdrücken durchsuchen", - "settings.permissions.tool.list.title": "Auflisten", - "settings.permissions.tool.list.description": "Dateien in einem Verzeichnis auflisten", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "Shell-Befehle ausführen", - "settings.permissions.tool.task.title": "Aufgabe", - "settings.permissions.tool.task.description": "Unteragenten starten", - "settings.permissions.tool.skill.title": "Fähigkeit", - "settings.permissions.tool.skill.description": "Eine Fähigkeit nach Namen laden", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "Language-Server-Abfragen ausführen", - "settings.permissions.tool.todowrite.title": "Todo schreiben", - "settings.permissions.tool.todowrite.description": "Die Todo-Liste aktualisieren", - "settings.permissions.tool.webfetch.title": "Web-Abruf", - "settings.permissions.tool.webfetch.description": "Inhalt von einer URL abrufen", - "settings.permissions.tool.websearch.title": "Web-Suche", - "settings.permissions.tool.websearch.description": "Das Web durchsuchen", - "settings.permissions.tool.codesearch.title": "Code-Suche", - "settings.permissions.tool.codesearch.description": "Code im Web durchsuchen", - "settings.permissions.tool.external_directory.title": "Externes Verzeichnis", - "settings.permissions.tool.external_directory.description": "Zugriff auf Dateien außerhalb des Projektverzeichnisses", - "settings.permissions.tool.doom_loop.title": "Doom Loop", - "settings.permissions.tool.doom_loop.description": "Wiederholte Tool-Aufrufe mit identischer Eingabe erkennen", - "session.delete.failed.title": "Sitzung konnte nicht gelöscht werden", - "session.delete.title": "Sitzung löschen", - "session.delete.confirm": 'Sitzung "{{name}}" löschen?', - "session.delete.button": "Sitzung löschen", - "workspace.new": "Neuer Arbeitsbereich", - "workspace.type.local": "lokal", - "workspace.type.sandbox": "Sandbox", - "workspace.create.failed.title": "Arbeitsbereich konnte nicht erstellt werden", - "workspace.delete.failed.title": "Arbeitsbereich konnte nicht gelöscht werden", - "workspace.resetting.title": "Arbeitsbereich wird zurückgesetzt", - "workspace.resetting.description": "Dies kann eine Minute dauern.", - "workspace.reset.failed.title": "Arbeitsbereich konnte nicht zurückgesetzt werden", - "workspace.reset.success.title": "Arbeitsbereich zurückgesetzt", - "workspace.reset.success.description": "Der Arbeitsbereich entspricht jetzt dem Standard-Branch.", - "workspace.error.stillPreparing": "Arbeitsbereich wird noch vorbereitet", - "workspace.status.checking": "Suche nach nicht zusammengeführten Änderungen...", - "workspace.status.error": "Git-Status konnte nicht überprüft werden.", - "workspace.status.clean": "Keine nicht zusammengeführten Änderungen erkannt.", - "workspace.status.dirty": "Nicht zusammengeführte Änderungen in diesem Arbeitsbereich erkannt.", - "workspace.delete.title": "Arbeitsbereich löschen", - "workspace.delete.confirm": 'Arbeitsbereich "{{name}}" löschen?', - "workspace.delete.button": "Arbeitsbereich löschen", - "workspace.reset.title": "Arbeitsbereich zurücksetzen", - "workspace.reset.confirm": 'Arbeitsbereich "{{name}}" zurücksetzen?', - "workspace.reset.button": "Arbeitsbereich zurücksetzen", - "workspace.reset.archived.none": "Keine aktiven Sitzungen werden archiviert.", - "workspace.reset.archived.one": "1 Sitzung wird archiviert.", - "workspace.reset.archived.many": "{{count}} Sitzungen werden archiviert.", - "workspace.reset.note": "Dadurch wird der Arbeitsbereich auf den Standard-Branch zurückgesetzt.", - "common.open": "Öffnen", - "dialog.releaseNotes.action.getStarted": "Loslegen", - "dialog.releaseNotes.action.next": "Weiter", - "dialog.releaseNotes.action.hideFuture": "In Zukunft nicht mehr anzeigen", - "dialog.releaseNotes.media.alt": "Vorschau auf die Version", - "toast.project.reloadFailed.title": "Fehler beim Neuladen von {{project}}", - "error.server.invalidConfiguration": "Ungültige Konfiguration", - "common.moreCountSuffix": " (+{{count}} weitere)", - "common.time.justNow": "Gerade eben", - "common.time.minutesAgo.short": "vor {{count}} Min", - "common.time.hoursAgo.short": "vor {{count}} Std", - "common.time.daysAgo.short": "vor {{count}} Tg", - "settings.providers.connected.environmentDescription": "Verbunden aus Ihren Umgebungsvariablen", - "settings.providers.custom.description": "Fügen Sie einen OpenAI-kompatiblen Anbieter per Basis-URL hinzu.", - - "app.server.unreachable": "Konnte {{server}} nicht erreichen", - "app.server.retrying": "Automatische erneute Verbindung...", - "app.server.otherServers": "Andere Server", - "dialog.server.add.usernamePlaceholder": "Benutzername", - "dialog.server.add.passwordPlaceholder": "Passwort", - "server.row.noUsername": "Kein Benutzername", - "session.review.noVcs.createGit.title": "Git-Repository erstellen", - "session.review.noVcs.createGit.description": - "Änderungen in diesem Projekt verfolgen, überprüfen und rückgängig machen", - "session.review.noVcs.createGit.actionLoading": "Git-Repository wird erstellt...", - "session.review.noVcs.createGit.action": "Git-Repository erstellen", - "session.todo.progress": "{{done}} von {{total}} Aufgaben erledigt", - "session.question.progress": "{{current}} von {{total}} Fragen", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "Datei-Explorer", - "session.header.open.fileManager": "Dateimanager", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "Terminal", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "Entwicklungs-Leistungsdiagnose", - "debugBar.na": "n.v.", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": - "Letzter abgeschlossener Routenübergang, der eine Sitzungsseite berührt, gemessen vom Start des Routers bis zum ersten Rendern nach dem Einschwingen.", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "Gleitende Bilder pro Sekunde in den letzten 5 Sekunden.", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "Schlechteste Frame-Zeit in den letzten 5 Sekunden.", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "Frames über 32ms in den letzten 5 Sekunden.", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "Blockierte Zeit und Anzahl langer Aufgaben in den letzten 5 Sekunden. Max Aufgabe: {{max}}.", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "Schlechteste beobachtete Eingabeverzögerung in den letzten 5 Sekunden.", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": - "Ungefähre Interaktionsdauer in den letzten 5 Sekunden. Dies ist INP-ähnlich, nicht das offizielle Web Vitals INP.", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "Kumulative Layoutverschiebung für die aktuelle App-Lebensdauer.", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "Verwendeter JS-Heap vs Heap-Limit. Nur Chromium.", - "debugBar.mem.tip": "Verwendeter JS-Heap vs Heap-Limit. {{used}} von {{limit}}.", - "common.key.ctrl": "Strg", - "common.key.alt": "Alt", - "common.key.shift": "Umschalt", - "common.key.meta": "Meta", - "common.key.space": "Leertaste", - "common.key.backspace": "Rücktaste", - "common.key.enter": "Eingabe", - "common.key.tab": "Tab", - "common.key.delete": "Entf", - "common.key.home": "Pos1", - "common.key.end": "Ende", - "common.key.pageUp": "Bild auf", - "common.key.pageDown": "Bild ab", - "common.key.insert": "Einfg", - "common.unknown": "unbekannt", - "error.page.circular": "[Zirkulär]", - "error.globalSDK.noServerAvailable": "Kein Server verfügbar", - "error.globalSDK.serverNotAvailable": "Server nicht verfügbar", - "error.childStore.persistedCacheCreateFailed": "Dauerhafter Cache konnte nicht erstellt werden", - "error.childStore.persistedProjectMetadataCreateFailed": "Dauerhafte Projektmetadaten konnten nicht erstellt werden", - "error.childStore.persistedProjectIconCreateFailed": "Dauerhaftes Projekticon konnte nicht erstellt werden", - "error.childStore.storeCreateFailed": "Speicher konnte nicht erstellt werden", - "terminal.connectionLost.abnormalClose": "WebSocket abnormal geschlossen: {{code}}", -} satisfies Partial> diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts deleted file mode 100644 index 6cde9741882..00000000000 --- a/packages/app/src/i18n/en.ts +++ /dev/null @@ -1,955 +0,0 @@ -export const dict = { - "command.category.suggested": "Suggested", - "command.category.view": "View", - "command.category.project": "Project", - "command.category.provider": "Provider", - "command.category.server": "Server", - "command.category.session": "Session", - "command.category.theme": "Theme", - "command.category.language": "Language", - "command.category.file": "File", - "command.category.context": "Context", - "command.category.terminal": "Terminal", - "command.category.model": "Model", - "command.category.mcp": "MCP", - "command.category.agent": "Agent", - "command.category.permissions": "Permissions", - "command.category.workspace": "Workspace", - "command.category.settings": "Settings", - - "theme.scheme.system": "System", - "theme.scheme.light": "Light", - "theme.scheme.dark": "Dark", - - "command.sidebar.toggle": "Toggle sidebar", - "command.project.open": "Open project", - "command.project.previous": "Previous project", - "command.project.next": "Next project", - "command.provider.connect": "Connect provider", - "command.server.switch": "Switch server", - "command.settings.open": "Open settings", - "command.session.previous": "Previous session", - "command.session.next": "Next session", - "command.session.previous.unseen": "Previous unread session", - "command.session.next.unseen": "Next unread session", - "command.session.archive": "Archive session", - - "command.palette": "Command palette", - - "command.theme.cycle": "Cycle theme", - "command.theme.set": "Use theme: {{theme}}", - "command.theme.scheme.cycle": "Cycle color scheme", - "command.theme.scheme.set": "Use color scheme: {{scheme}}", - - "command.language.cycle": "Cycle language", - "command.language.set": "Use language: {{language}}", - - "command.session.new": "New session", - "command.file.open": "Open file", - "command.tab.close": "Close tab", - "command.context.addSelection": "Add selection to context", - "command.context.addSelection.description": "Add selected lines from the current file", - "command.input.focus": "Focus input", - "command.terminal.toggle": "Toggle terminal", - "command.fileTree.toggle": "Toggle file tree", - "command.review.toggle": "Toggle review", - "command.terminal.new": "New terminal", - "command.terminal.new.description": "Create a new terminal tab", - "command.steps.toggle": "Toggle steps", - "command.steps.toggle.description": "Show or hide steps for the current message", - "command.message.previous": "Previous message", - "command.message.previous.description": "Go to the previous user message", - "command.message.next": "Next message", - "command.message.next.description": "Go to the next user message", - "command.model.choose": "Choose model", - "command.model.choose.description": "Select a different model", - "command.mcp.toggle": "Toggle MCPs", - "command.mcp.toggle.description": "Toggle MCPs", - "command.agent.cycle": "Cycle agent", - "command.agent.cycle.description": "Switch to the next agent", - "command.agent.cycle.reverse": "Cycle agent backwards", - "command.agent.cycle.reverse.description": "Switch to the previous agent", - "command.model.variant.cycle": "Cycle thinking effort", - "command.model.variant.cycle.description": "Switch to the next effort level", - "command.prompt.mode.shell": "Shell", - "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Auto-accept permissions", - "command.permissions.autoaccept.disable": "Stop auto-accepting permissions", - "command.workspace.toggle": "Toggle workspaces", - "command.workspace.toggle.description": "Enable or disable multiple workspaces in the sidebar", - "command.session.undo": "Undo", - "command.session.undo.description": "Undo the last message", - "command.session.redo": "Redo", - "command.session.redo.description": "Redo the last undone message", - "command.session.compact": "Compact session", - "command.session.compact.description": "Summarize the session to reduce context size", - "command.session.fork": "Fork from message", - "command.session.fork.description": "Create a new session from a previous message", - "command.session.share": "Share session", - "command.session.share.description": "Share this session and copy the URL to clipboard", - "command.session.unshare": "Unshare session", - "command.session.unshare.description": "Stop sharing this session", - - "palette.search.placeholder": "Search files, commands, and sessions", - "palette.empty": "No results found", - "palette.group.commands": "Commands", - "palette.group.files": "Files", - - "dialog.provider.search.placeholder": "Search providers", - "dialog.provider.empty": "No providers found", - "dialog.provider.group.popular": "Popular", - "dialog.provider.group.other": "Other", - "dialog.provider.tag.recommended": "Recommended", - "dialog.provider.opencode.note": "Curated models including Claude, GPT, Gemini and more", - "dialog.provider.opencode.tagline": "Reliable optimized models", - "dialog.provider.opencodeGo.tagline": "Low cost subscription for everyone", - "dialog.provider.anthropic.note": "Direct access to Claude models, including Pro and Max", - "dialog.provider.copilot.note": "AI models for coding assistance via GitHub Copilot", - "dialog.provider.openai.note": "GPT models for fast, capable general AI tasks", - "dialog.provider.google.note": "Gemini models for fast, structured responses", - "dialog.provider.openrouter.note": "Access all supported models from one provider", - "dialog.provider.vercel.note": "Unified access to AI models with smart routing", - - "dialog.model.select.title": "Select model", - "dialog.model.search.placeholder": "Search models", - "dialog.model.empty": "No model results", - "dialog.model.manage": "Manage models", - "dialog.model.manage.description": "Customize which models appear in the model selector.", - "dialog.model.manage.provider.toggle": "Toggle all {{provider}} models", - - "dialog.model.unpaid.freeModels.title": "Free models provided by Kilo", - "dialog.model.unpaid.addMore.title": "Add more models from popular providers", - - "dialog.provider.viewAll": "Show more providers", - - "provider.connect.title": "Connect {{provider}}", - "provider.connect.title.anthropicProMax": "Login with Claude Pro/Max", - "provider.connect.selectMethod": "Select login method for {{provider}}.", - "provider.connect.method.apiKey": "API key", - "provider.connect.status.inProgress": "Authorization in progress...", - "provider.connect.status.waiting": "Waiting for authorization...", - "provider.connect.status.failed": "Authorization failed: {{error}}", - "provider.connect.apiKey.description": - "Enter your {{provider}} API key to connect your account and use {{provider}} models in Kilo.", - "provider.connect.apiKey.label": "{{provider}} API key", - "provider.connect.apiKey.placeholder": "API key", - "provider.connect.apiKey.required": "API key is required", - "provider.connect.opencodeZen.line1": - "OpenCode Zen gives you access to a curated set of reliable optimized models for coding agents.", - "provider.connect.opencodeZen.line2": - "With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.", - "provider.connect.opencodeZen.visit.prefix": "Visit ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " to collect your API key.", - "provider.connect.oauth.code.visit.prefix": "Visit ", - "provider.connect.oauth.code.visit.link": "this link", - "provider.connect.oauth.code.visit.suffix": - " to collect your authorization code to connect your account and use {{provider}} models in Kilo.", - "provider.connect.oauth.code.label": "{{method}} authorization code", - "provider.connect.oauth.code.placeholder": "Authorization code", - "provider.connect.oauth.code.required": "Authorization code is required", - "provider.connect.oauth.code.invalid": "Invalid authorization code", - "provider.connect.oauth.auto.visit.prefix": "Visit ", - "provider.connect.oauth.auto.visit.link": "this link", - "provider.connect.oauth.auto.visit.suffix": - " and enter the code below to connect your account and use {{provider}} models in Kilo.", - "provider.connect.oauth.auto.confirmationCode": "Confirmation code", - "provider.connect.toast.connected.title": "{{provider}} connected", - "provider.connect.toast.connected.description": "{{provider}} models are now available to use.", - - "provider.custom.title": "Custom provider", - "provider.custom.description.prefix": "Configure an OpenAI-compatible provider. See the ", - "provider.custom.description.link": "provider config docs", - "provider.custom.description.suffix": ".", - "provider.custom.field.providerID.label": "Provider ID", - "provider.custom.field.providerID.placeholder": "myprovider", - "provider.custom.field.providerID.description": "Lowercase letters, numbers, hyphens, or underscores", - "provider.custom.field.name.label": "Display name", - "provider.custom.field.name.placeholder": "My AI Provider", - "provider.custom.field.baseURL.label": "Base URL", - "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", - "provider.custom.field.apiKey.label": "API key", - "provider.custom.field.apiKey.placeholder": "API key", - "provider.custom.field.apiKey.description": "Optional. Leave empty if you manage auth via headers.", - "provider.custom.models.label": "Models", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "model-id", - "provider.custom.models.name.label": "Name", - "provider.custom.models.name.placeholder": "Display Name", - "provider.custom.models.remove": "Remove model", - "provider.custom.models.add": "Add model", - "provider.custom.headers.label": "Headers (optional)", - "provider.custom.headers.key.label": "Header", - "provider.custom.headers.key.placeholder": "Header-Name", - "provider.custom.headers.value.label": "Value", - "provider.custom.headers.value.placeholder": "value", - "provider.custom.headers.remove": "Remove header", - "provider.custom.headers.add": "Add header", - "provider.custom.error.providerID.required": "Provider ID is required", - "provider.custom.error.providerID.format": "Use lowercase letters, numbers, hyphens, or underscores", - "provider.custom.error.providerID.exists": "That provider ID already exists", - "provider.custom.error.name.required": "Display name is required", - "provider.custom.error.baseURL.required": "Base URL is required", - "provider.custom.error.baseURL.format": "Must start with http:// or https://", - "provider.custom.error.required": "Required", - "provider.custom.error.duplicate": "Duplicate", - - "provider.disconnect.toast.disconnected.title": "{{provider}} disconnected", - "provider.disconnect.toast.disconnected.description": "{{provider}} models are no longer available.", - - "model.tag.free": "Free", - "model.tag.latest": "Latest", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "text", - "model.input.image": "image", - "model.input.audio": "audio", - "model.input.video": "video", - "model.input.pdf": "pdf", - "model.tooltip.allows": "Allows: {{inputs}}", - "model.tooltip.reasoning.allowed": "Allows reasoning", - "model.tooltip.reasoning.none": "No reasoning", - "model.tooltip.context": "Context limit {{limit}}", - - "common.search.placeholder": "Search", - "common.goBack": "Navigate back", - "common.goForward": "Navigate forward", - "common.loading": "Loading", - "common.loading.ellipsis": "...", - "common.cancel": "Cancel", - "common.open": "Open", - "common.connect": "Connect", - "common.disconnect": "Disconnect", - "common.continue": "Continue", - "common.submit": "Submit", - "common.save": "Save", - "common.saving": "Saving...", - "common.default": "Default", - "common.attachment": "attachment", - - "prompt.placeholder.shell": "Enter shell command... {{example}}", - "prompt.placeholder.normal": 'Ask anything... "{{example}}"', - "prompt.placeholder.simple": "Ask anything...", - "prompt.placeholder.summarizeComments": "Summarize comments…", - "prompt.placeholder.summarizeComment": "Summarize comment…", - "prompt.mode.shell": "Shell", - "prompt.mode.normal": "Prompt", - "prompt.mode.shell.exit": "esc to exit", - "session.child.promptDisabled": "Subagent sessions cannot be prompted.", - "session.child.backToParent": "Back to main session.", - - "prompt.example.1": "Fix a TODO in the codebase", - "prompt.example.2": "What is the tech stack of this project?", - "prompt.example.3": "Fix broken tests", - "prompt.example.4": "Explain how authentication works", - "prompt.example.5": "Find and fix security vulnerabilities", - "prompt.example.6": "Add unit tests for the user service", - "prompt.example.7": "Refactor this function to be more readable", - "prompt.example.8": "What does this error mean?", - "prompt.example.9": "Help me debug this issue", - "prompt.example.10": "Generate API documentation", - "prompt.example.11": "Optimize database queries", - "prompt.example.12": "Add input validation", - "prompt.example.13": "Create a new component for...", - "prompt.example.14": "How do I deploy this project?", - "prompt.example.15": "Review my code for best practices", - "prompt.example.16": "Add error handling to this function", - "prompt.example.17": "Explain this regex pattern", - "prompt.example.18": "Convert this to TypeScript", - "prompt.example.19": "Add logging throughout the codebase", - "prompt.example.20": "What dependencies are outdated?", - "prompt.example.21": "Help me write a migration script", - "prompt.example.22": "Implement caching for this endpoint", - "prompt.example.23": "Add pagination to this list", - "prompt.example.24": "Create a CLI command for...", - "prompt.example.25": "How do environment variables work here?", - - "prompt.popover.emptyResults": "No matching results", - "prompt.popover.emptyCommands": "No matching commands", - "prompt.dropzone.label": "Drop images, PDFs, or text files here", - "prompt.dropzone.file.label": "Drop to @mention file", - "prompt.slash.badge.custom": "custom", - "prompt.slash.badge.skill": "skill", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "active", - "prompt.context.includeActiveFile": "Include active file", - "prompt.context.removeActiveFile": "Remove active file from context", - "prompt.context.removeFile": "Remove file from context", - "prompt.action.attachFile": "Add files", - "prompt.attachment.remove": "Remove attachment", - "prompt.action.send": "Send", - "prompt.action.stop": "Stop", - - "prompt.toast.pasteUnsupported.title": "Unsupported attachment", - "prompt.toast.pasteUnsupported.description": "Only images, PDFs, or text files can be attached here.", - "prompt.toast.modelAgentRequired.title": "Select an agent and model", - "prompt.toast.modelAgentRequired.description": "Choose an agent and model before sending a prompt.", - "prompt.toast.worktreeCreateFailed.title": "Failed to create worktree", - "prompt.toast.sessionCreateFailed.title": "Failed to create session", - "prompt.toast.shellSendFailed.title": "Failed to send shell command", - "prompt.toast.commandSendFailed.title": "Failed to send command", - "prompt.toast.promptSendFailed.title": "Failed to send prompt", - "prompt.toast.promptSendFailed.description": "Unable to retrieve session", - - "dialog.mcp.title": "MCPs", - "dialog.mcp.description": "{{enabled}} of {{total}} enabled", - "dialog.mcp.empty": "No MCPs configured", - - "dialog.lsp.empty": "LSPs auto-detected from file types", - "dialog.plugins.empty": "Plugins configured in opencode.json", - - "mcp.status.connected": "connected", - "mcp.status.failed": "failed", - "mcp.status.needs_auth": "needs auth", - "mcp.status.disabled": "disabled", - - "dialog.fork.empty": "No messages to fork from", - - "dialog.directory.search.placeholder": "Search folders", - "dialog.directory.empty": "No folders found", - - "app.server.unreachable": "Could not reach {{server}}", - "app.server.retrying": "Retrying automatically...", - "app.server.otherServers": "Other servers", - - "dialog.server.title": "Servers", - "dialog.server.description": "Switch which Kilo server this app connects to.", - "dialog.server.search.placeholder": "Search servers", - "dialog.server.empty": "No servers yet", - "dialog.server.add.title": "Add server", - "dialog.server.add.url": "Server address", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "Could not connect to server", - "dialog.server.add.checking": "Checking...", - "dialog.server.add.button": "Add server", - "dialog.server.add.name": "Server name (optional)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "Username (optional)", - "dialog.server.add.usernamePlaceholder": "username", - "dialog.server.add.password": "Password (optional)", - "dialog.server.add.passwordPlaceholder": "password", - "dialog.server.edit.title": "Edit server", - "dialog.server.default.title": "Default server", - "dialog.server.default.description": - "Connect to this server on app launch instead of starting a local server. Requires restart.", - "dialog.server.default.none": "No server selected", - "dialog.server.default.set": "Set current server as default", - "dialog.server.default.clear": "Clear", - "dialog.server.action.remove": "Remove server", - - "dialog.server.menu.edit": "Edit", - "dialog.server.menu.default": "Set as default", - "dialog.server.menu.defaultRemove": "Remove default", - "dialog.server.menu.delete": "Delete", - "dialog.server.current": "Current Server", - "dialog.server.status.default": "Default", - "server.row.noUsername": "no username", - - "dialog.project.edit.title": "Edit project", - "dialog.project.edit.name": "Name", - "dialog.project.edit.icon": "Icon", - "dialog.project.edit.icon.alt": "Project icon", - "dialog.project.edit.icon.hint": "Click or drag an image", - "dialog.project.edit.icon.recommended": "Recommended: 128x128px", - "dialog.project.edit.color": "Color", - "dialog.project.edit.color.select": "Select {{color}} color", - "dialog.project.edit.worktree.startup": "Workspace startup script", - "dialog.project.edit.worktree.startup.description": "Runs after creating a new workspace (worktree).", - "dialog.project.edit.worktree.startup.placeholder": "e.g. bun install", - - "dialog.releaseNotes.action.getStarted": "Get started", - "dialog.releaseNotes.action.next": "Next", - "dialog.releaseNotes.action.hideFuture": "Don't show these in the future", - "dialog.releaseNotes.media.alt": "Release preview", - - "context.breakdown.title": "Context Breakdown", - "context.breakdown.note": 'Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.', - "context.breakdown.system": "System", - "context.breakdown.user": "User", - "context.breakdown.assistant": "Assistant", - "context.breakdown.tool": "Tool Calls", - "context.breakdown.other": "Other", - - "context.systemPrompt.title": "System Prompt", - "context.rawMessages.title": "Raw messages", - - "context.stats.session": "Session", - "context.stats.messages": "Messages", - "context.stats.provider": "Provider", - "context.stats.model": "Model", - "context.stats.limit": "Context Limit", - "context.stats.totalTokens": "Total Tokens", - "context.stats.usage": "Usage", - "context.stats.inputTokens": "Input Tokens", - "context.stats.outputTokens": "Output Tokens", - "context.stats.reasoningTokens": "Reasoning Tokens", - "context.stats.cacheTokens": "Cache Tokens (read/write)", - "context.stats.userMessages": "User Messages", - "context.stats.assistantMessages": "Assistant Messages", - "context.stats.totalCost": "Total Cost", - "context.stats.sessionCreated": "Session Created", - "context.stats.lastActivity": "Last Activity", - - "context.usage.tokens": "Tokens", - "context.usage.usage": "Usage", - "context.usage.cost": "Cost", - "context.usage.clickToView": "Click to view context", - "context.usage.view": "View context usage", - - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - - "toast.language.title": "Language", - "toast.language.description": "Switched to {{language}}", - - "toast.theme.title": "Theme switched", - "toast.scheme.title": "Color scheme", - - "toast.workspace.enabled.title": "Workspaces enabled", - "toast.workspace.enabled.description": "Multiple worktrees are now shown in the sidebar", - "toast.workspace.disabled.title": "Workspaces disabled", - "toast.workspace.disabled.description": "Only the main worktree is shown in the sidebar", - - "toast.permissions.autoaccept.on.title": "Auto-accepting permissions", - "toast.permissions.autoaccept.on.description": "Permission requests will be automatically approved", - "toast.permissions.autoaccept.off.title": "Stopped auto-accepting permissions", - "toast.permissions.autoaccept.off.description": "Permission requests will require approval", - - "toast.model.none.title": "No model selected", - "toast.model.none.description": "Connect a provider to summarize this session", - - "toast.file.loadFailed.title": "Failed to load file", - "toast.file.listFailed.title": "Failed to list files", - - "toast.context.noLineSelection.title": "No line selection", - "toast.context.noLineSelection.description": "Select a line range in a file tab first.", - - "toast.session.share.copyFailed.title": "Failed to copy URL to clipboard", - "toast.session.share.success.title": "Session shared", - "toast.session.share.success.description": "Share URL copied to clipboard!", - "toast.session.share.failed.title": "Failed to share session", - "toast.session.share.failed.description": "An error occurred while sharing the session", - - "toast.session.unshare.success.title": "Session unshared", - "toast.session.unshare.success.description": "Session unshared successfully!", - "toast.session.unshare.failed.title": "Failed to unshare session", - "toast.session.unshare.failed.description": "An error occurred while unsharing the session", - - "toast.session.listFailed.title": "Failed to load sessions for {{project}}", - "toast.project.reloadFailed.title": "Failed to reload {{project}}", - - "toast.update.title": "Update available", - "toast.update.description": "A new version of Kilo ({{version}}) is now available to install.", - "toast.update.action.installRestart": "Install and restart", - "toast.update.action.notYet": "Not yet", - - "error.page.title": "Something went wrong", - "error.page.description": "An error occurred while loading the application.", - "error.page.details.label": "Error Details", - "error.page.action.restart": "Restart", - "error.page.action.checking": "Checking...", - "error.page.action.checkUpdates": "Check for updates", - "error.page.action.updateTo": "Update to {{version}}", - "error.page.circular": "[Circular]", - "error.page.report.prefix": "Please report this error to the Kilo team", - "error.page.report.discord": "on Discord", - "error.page.version": "Version: {{version}}", - - "error.dev.rootNotFound": - "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?", - - "error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?", - "error.globalSDK.noServerAvailable": "No server available", - "error.globalSDK.serverNotAvailable": "Server not available", - "error.childStore.persistedCacheCreateFailed": "Failed to create persisted cache", - "error.childStore.persistedProjectMetadataCreateFailed": "Failed to create persisted project metadata", - "error.childStore.persistedProjectIconCreateFailed": "Failed to create persisted project icon", - "error.childStore.storeCreateFailed": "Failed to create store", - "directory.error.invalidUrl": "Invalid directory in URL.", - - "error.chain.unknown": "Unknown error", - "error.server.invalidConfiguration": "Invalid configuration", - "error.chain.causedBy": "Caused by:", - "error.chain.apiError": "API error", - "error.chain.status": "Status: {{status}}", - "error.chain.retryable": "Retryable: {{retryable}}", - "error.chain.responseBody": "Response body:\n{{body}}", - "error.chain.didYouMean": "Did you mean: {{suggestions}}", - "error.chain.modelNotFound": "Model not found: {{provider}}/{{model}}", - "error.chain.checkConfig": "Check your config (opencode.json) provider/model names", - "error.chain.mcpFailed": 'MCP server "{{name}}" failed. Note, Kilo does not support MCP authentication yet.', - "error.chain.providerAuthFailed": "Provider authentication failed ({{provider}}): {{message}}", - "error.chain.providerInitFailed": - 'Failed to initialize provider "{{provider}}". Check credentials and configuration.', - "error.chain.configJsonInvalid": "Config file at {{path}} is not valid JSON(C)", - "error.chain.configJsonInvalidWithMessage": "Config file at {{path}} is not valid JSON(C): {{message}}", - "error.chain.configDirectoryTypo": - 'Directory "{{dir}}" in {{path}} is not valid. Rename the directory to "{{suggestion}}" or remove it. This is a common typo.', - "error.chain.configFrontmatterError": "Failed to parse frontmatter in {{path}}:\n{{message}}", - "error.chain.configInvalid": "Config file at {{path}} is invalid", - "error.chain.configInvalidWithMessage": "Config file at {{path}} is invalid: {{message}}", - - "notification.permission.title": "Permission required", - "notification.permission.description": "{{sessionTitle}} in {{projectName}} needs permission", - "notification.question.title": "Question", - "notification.question.description": "{{sessionTitle}} in {{projectName}} has a question", - "notification.action.goToSession": "Go to session", - - "notification.session.responseReady.title": "Response ready", - "notification.session.error.title": "Session error", - "notification.session.error.fallbackDescription": "An error occurred", - - "home.recentProjects": "Recent projects", - "home.empty.title": "No recent projects", - "home.empty.description": "Get started by opening a local project", - - "session.tab.session": "Session", - "session.tab.review": "Review", - "session.tab.context": "Context", - "session.panel.reviewAndFiles": "Review and files", - "session.review.filesChanged": "{{count}} Files Changed", - "session.review.change.one": "Change", - "session.review.change.other": "Changes", - "session.review.loadingChanges": "Loading changes...", - "session.review.empty": "No changes in this session yet", - "session.review.noVcs": "No Git Version Control System detected, changes not displayed", - "session.review.noVcs.createGit.title": "Create a Git repository", - "session.review.noVcs.createGit.description": "Track, review, and undo changes in this project", - "session.review.noVcs.createGit.actionLoading": "Creating Git repository...", - "session.review.noVcs.createGit.action": "Create Git repository", - "session.review.noSnapshot": "Snapshot tracking is disabled in config, so session changes are unavailable", - "session.review.noChanges": "No changes", - "session.review.noUncommittedChanges": "No uncommitted changes yet", - "session.review.noBranchChanges": "No branch changes yet", - - "session.files.selectToOpen": "Select a file to open", - "session.files.all": "All files", - "session.files.empty": "No files", - "session.files.binaryContent": "Binary file (content cannot be displayed)", - - "session.messages.renderEarlier": "Render earlier messages", - "session.messages.loadingEarlier": "Loading earlier messages...", - "session.messages.loadEarlier": "Load earlier messages", - "session.messages.loading": "Loading messages...", - "session.messages.jumpToLatest": "Jump to latest", - - "session.context.addToContext": "Add {{selection}} to context", - "session.todo.title": "Todos", - "session.todo.collapse": "Collapse", - "session.todo.expand": "Expand", - "session.todo.progress": "{{done}} of {{total}} todos completed", - "session.question.progress": "{{current}} of {{total}} questions", - "session.followupDock.summary.one": "{{count}} queued message", - "session.followupDock.summary.other": "{{count}} queued messages", - "session.followupDock.sendNow": "Send now", - "session.followupDock.edit": "Edit", - "session.followupDock.collapse": "Collapse queued messages", - "session.followupDock.expand": "Expand queued messages", - "session.revertDock.summary.one": "{{count}} rolled back message", - "session.revertDock.summary.other": "{{count}} rolled back messages", - "session.revertDock.collapse": "Collapse rolled back messages", - "session.revertDock.expand": "Expand rolled back messages", - "session.revertDock.restore": "Restore message", - - "session.new.title": "Build anything", - "session.new.worktree.main": "Main branch", - "session.new.worktree.mainWithBranch": "Main branch ({{branch}})", - "session.new.worktree.create": "Create new worktree", - "session.new.lastModified": "Last modified", - - "session.header.search.placeholder": "Search {{project}}", - "session.header.searchFiles": "Search files", - "session.header.openIn": "Open in", - "session.header.open.action": "Open {{app}}", - "session.header.open.ariaLabel": "Open in {{app}}", - "session.header.open.menu": "Open options", - "session.header.open.copyPath": "Copy path", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "File Explorer", - "session.header.open.fileManager": "File Manager", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "Terminal", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - - "status.popover.trigger": "Status", - "status.popover.ariaLabel": "Server configurations", - "status.popover.tab.servers": "Servers", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "Plugins", - "status.popover.action.manageServers": "Manage servers", - - "session.share.popover.title": "Publish on web", - "session.share.popover.description.shared": - "This session is public on the web. It is accessible to anyone with the link.", - "session.share.popover.description.unshared": - "Share session publicly on the web. It will be accessible to anyone with the link.", - "session.share.action.share": "Share", - "session.share.action.publish": "Publish", - "session.share.action.publishing": "Publishing...", - "session.share.action.unpublish": "Unpublish", - "session.share.action.unpublishing": "Unpublishing...", - "session.share.action.view": "View", - "session.share.copy.copied": "Copied", - "session.share.copy.copyLink": "Copy link", - - "lsp.tooltip.none": "No LSP servers", - "lsp.label.connected": "{{count}} LSP", - - "prompt.loading": "Loading prompt...", - "terminal.loading": "Loading terminal...", - "terminal.title": "Terminal", - "terminal.title.numbered": "Terminal {{number}}", - "terminal.close": "Close terminal", - "terminal.connectionLost.title": "Connection Lost", - "terminal.connectionLost.abnormalClose": "WebSocket closed abnormally: {{code}}", - "terminal.connectionLost.description": - "The terminal connection was interrupted. This can happen when the server restarts.", - - "common.closeTab": "Close tab", - "common.dismiss": "Dismiss", - "common.moreCountSuffix": " (+{{count}} more)", - "common.requestFailed": "Request failed", - "common.moreOptions": "More options", - "common.learnMore": "Learn more", - "common.rename": "Rename", - "common.reset": "Reset", - "common.archive": "Archive", - "common.delete": "Delete", - "common.close": "Close", - "common.edit": "Edit", - "common.loadMore": "Load more", - "common.key.esc": "ESC", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "Space", - "common.key.backspace": "Backspace", - "common.key.enter": "Enter", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "unknown", - - "common.time.justNow": "Just now", - "common.time.minutesAgo.short": "{{count}}m ago", - "common.time.hoursAgo.short": "{{count}}h ago", - "common.time.daysAgo.short": "{{count}}d ago", - - "sidebar.menu.toggle": "Toggle menu", - "sidebar.nav.projectsAndSessions": "Projects and sessions", - "sidebar.settings": "Settings", - "sidebar.help": "Help", - "sidebar.workspaces.enable": "Enable workspaces", - "sidebar.workspaces.disable": "Disable workspaces", - "sidebar.gettingStarted.title": "Getting started", - "sidebar.gettingStarted.line1": "Kilo includes free models so you can start immediately.", - "sidebar.gettingStarted.line2": "Connect any provider to use models, inc. Claude, GPT, Gemini etc.", - "sidebar.project.recentSessions": "Recent sessions", - "sidebar.project.viewAllSessions": "View all sessions", - "sidebar.project.clearNotifications": "Clear notifications", - "sidebar.empty.title": "No projects open", - "sidebar.empty.description": "Open a project to get started", - - "debugBar.ariaLabel": "Development performance diagnostics", - "debugBar.na": "n/a", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": - "Last completed route transition touching a session page, measured from router start until the first paint after it settles.", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "Rolling frames per second over the last 5 seconds.", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "Worst frame time over the last 5 seconds.", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "Frames over 32ms in the last 5 seconds.", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "Blocked time and long-task count in the last 5 seconds. Max task: {{max}}.", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "Worst observed input delay in the last 5 seconds.", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": - "Approximate interaction duration over the last 5 seconds. This is INP-like, not the official Web Vitals INP.", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "Cumulative layout shift for the current app lifetime.", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "Used JS heap vs heap limit. Chromium only.", - "debugBar.mem.tip": "Used JS heap vs heap limit. {{used}} of {{limit}}.", - - "app.name.desktop": "Kilo Desktop", - - "settings.section.desktop": "Desktop", - "settings.section.server": "Server", - "settings.tab.general": "General", - "settings.tab.shortcuts": "Shortcuts", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL integration", - "settings.desktop.wsl.description": "Run the Kilo server inside WSL on Windows.", - - "settings.general.section.appearance": "Appearance", - "settings.general.section.advanced": "Advanced", - "settings.general.section.notifications": "System notifications", - "settings.general.section.updates": "Updates", - "settings.general.section.sounds": "Sound effects", - "settings.general.section.feed": "Feed", - "settings.general.section.display": "Display", - - "settings.general.row.language.title": "Language", - "settings.general.row.language.description": "Change the display language for Kilo", - "settings.general.row.appearance.title": "Appearance", - "settings.general.row.appearance.description": "Customise how Kilo looks on your device", - "settings.general.row.colorScheme.title": "Color scheme", - "settings.general.row.colorScheme.description": "Choose whether Kilo follows the system, light, or dark theme", - "settings.general.row.theme.title": "Theme", - "settings.general.row.theme.description": "Customise how Kilo is themed.", - "settings.general.row.font.title": "Code Font", - "settings.general.row.font.description": "Customise the font used in code blocks", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "UI Font", - "settings.general.row.uiFont.description": "Customise the font used throughout the interface", - "settings.general.row.followup.title": "Follow-up behavior", - "settings.general.row.followup.description": "Choose whether follow-up prompts steer immediately or wait in a queue", - "settings.general.row.followup.option.queue": "Queue", - "settings.general.row.followup.option.steer": "Steer", - "settings.general.row.showFileTree.title": "File tree", - "settings.general.row.showFileTree.description": "Show the file tree toggle and panel in desktop sessions", - "settings.general.row.showNavigation.title": "Navigation controls", - "settings.general.row.showNavigation.description": "Show the back and forward buttons in the desktop title bar", - "settings.general.row.showSearch.title": "Command palette", - "settings.general.row.showSearch.description": "Show the search and command palette button in the desktop title bar", - "settings.general.row.showTerminal.title": "Terminal", - "settings.general.row.showTerminal.description": "Show the terminal button in the desktop title bar", - "settings.general.row.showStatus.title": "Server status", - "settings.general.row.showStatus.description": "Show the server status button in the desktop title bar", - "settings.general.row.reasoningSummaries.title": "Show reasoning summaries", - "settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline", - "settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts", - "settings.general.row.shellToolPartsExpanded.description": - "Show shell tool parts expanded by default in the timeline", - "settings.general.row.editToolPartsExpanded.title": "Expand edit tool parts", - "settings.general.row.editToolPartsExpanded.description": - "Show edit, write, and patch tool parts expanded by default in the timeline", - "settings.general.row.showSessionProgressBar.title": "Show session progress bar", - "settings.general.row.showSessionProgressBar.description": - "Display the animated progress bar at the top of the session when the agent is working", - - "settings.general.row.wayland.title": "Use native Wayland", - "settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.", - "settings.general.row.wayland.tooltip": - "On Linux with mixed refresh-rate monitors, native Wayland can be more stable.", - - "settings.general.row.releaseNotes.title": "Release notes", - "settings.general.row.releaseNotes.description": "Show What's New popups after updates", - - "settings.updates.row.startup.title": "Check for updates on startup", - "settings.updates.row.startup.description": "Automatically check for updates when Kilo launches", - "settings.updates.row.check.title": "Check for updates", - "settings.updates.row.check.description": "Manually check for updates and install if available", - "settings.updates.action.checkNow": "Check now", - "settings.updates.action.checking": "Checking...", - "settings.updates.toast.latest.title": "You're up to date", - "settings.updates.toast.latest.description": "You're running the latest version of Kilo.", - "sound.option.none": "None", - "sound.option.alert01": "Alert 01", - "sound.option.alert02": "Alert 02", - "sound.option.alert03": "Alert 03", - "sound.option.alert04": "Alert 04", - "sound.option.alert05": "Alert 05", - "sound.option.alert06": "Alert 06", - "sound.option.alert07": "Alert 07", - "sound.option.alert08": "Alert 08", - "sound.option.alert09": "Alert 09", - "sound.option.alert10": "Alert 10", - "sound.option.bipbop01": "Bip-bop 01", - "sound.option.bipbop02": "Bip-bop 02", - "sound.option.bipbop03": "Bip-bop 03", - "sound.option.bipbop04": "Bip-bop 04", - "sound.option.bipbop05": "Bip-bop 05", - "sound.option.bipbop06": "Bip-bop 06", - "sound.option.bipbop07": "Bip-bop 07", - "sound.option.bipbop08": "Bip-bop 08", - "sound.option.bipbop09": "Bip-bop 09", - "sound.option.bipbop10": "Bip-bop 10", - "sound.option.staplebops01": "Staplebops 01", - "sound.option.staplebops02": "Staplebops 02", - "sound.option.staplebops03": "Staplebops 03", - "sound.option.staplebops04": "Staplebops 04", - "sound.option.staplebops05": "Staplebops 05", - "sound.option.staplebops06": "Staplebops 06", - "sound.option.staplebops07": "Staplebops 07", - "sound.option.nope01": "Nope 01", - "sound.option.nope02": "Nope 02", - "sound.option.nope03": "Nope 03", - "sound.option.nope04": "Nope 04", - "sound.option.nope05": "Nope 05", - "sound.option.nope06": "Nope 06", - "sound.option.nope07": "Nope 07", - "sound.option.nope08": "Nope 08", - "sound.option.nope09": "Nope 09", - "sound.option.nope10": "Nope 10", - "sound.option.nope11": "Nope 11", - "sound.option.nope12": "Nope 12", - "sound.option.yup01": "Yup 01", - "sound.option.yup02": "Yup 02", - "sound.option.yup03": "Yup 03", - "sound.option.yup04": "Yup 04", - "sound.option.yup05": "Yup 05", - "sound.option.yup06": "Yup 06", - - "settings.general.notifications.agent.title": "Agent", - "settings.general.notifications.agent.description": - "Show system notification when the agent is complete or needs attention", - "settings.general.notifications.permissions.title": "Permissions", - "settings.general.notifications.permissions.description": "Show system notification when a permission is required", - "settings.general.notifications.errors.title": "Errors", - "settings.general.notifications.errors.description": "Show system notification when an error occurs", - - "settings.general.sounds.agent.title": "Agent", - "settings.general.sounds.agent.description": "Play sound when the agent is complete or needs attention", - "settings.general.sounds.permissions.title": "Permissions", - "settings.general.sounds.permissions.description": "Play sound when a permission is required", - "settings.general.sounds.errors.title": "Errors", - "settings.general.sounds.errors.description": "Play sound when an error occurs", - - "settings.shortcuts.title": "Keyboard shortcuts", - "settings.shortcuts.reset.button": "Reset to defaults", - "settings.shortcuts.reset.toast.title": "Shortcuts reset", - "settings.shortcuts.reset.toast.description": "Keyboard shortcuts have been reset to defaults.", - "settings.shortcuts.conflict.title": "Shortcut already in use", - "settings.shortcuts.conflict.description": "{{keybind}} is already assigned to {{titles}}.", - "settings.shortcuts.unassigned": "Unassigned", - "settings.shortcuts.pressKeys": "Press keys", - "settings.shortcuts.search.placeholder": "Search shortcuts", - "settings.shortcuts.search.empty": "No shortcuts found", - - "settings.shortcuts.group.general": "General", - "settings.shortcuts.group.session": "Session", - "settings.shortcuts.group.navigation": "Navigation", - "settings.shortcuts.group.modelAndAgent": "Model and agent", - "settings.shortcuts.group.terminal": "Terminal", - "settings.shortcuts.group.prompt": "Prompt", - - "settings.providers.title": "Providers", - "settings.providers.description": "Provider settings will be configurable here.", - "settings.providers.section.connected": "Connected providers", - "settings.providers.connected.empty": "No connected providers", - "settings.providers.connected.environmentDescription": "Connected from your environment variables", - "settings.providers.section.popular": "Popular providers", - "settings.providers.custom.description": "Add an OpenAI-compatible provider by base URL.", - "settings.providers.tag.environment": "Environment", - "settings.providers.tag.config": "Config", - "settings.providers.tag.custom": "Custom", - "settings.providers.tag.other": "Other", - "settings.models.title": "Models", - "settings.models.description": "Model settings will be configurable here.", - "settings.agents.title": "Agents", - "settings.agents.description": "Agent settings will be configurable here.", - "settings.commands.title": "Commands", - "settings.commands.description": "Command settings will be configurable here.", - "settings.mcp.title": "MCP", - "settings.mcp.description": "MCP settings will be configurable here.", - - "settings.permissions.title": "Permissions", - "settings.permissions.description": "Control what tools the server can use by default.", - "settings.permissions.section.tools": "Tools", - "settings.permissions.toast.updateFailed.title": "Failed to update permissions", - - "settings.permissions.action.allow": "Allow", - "settings.permissions.action.ask": "Ask", - "settings.permissions.action.deny": "Deny", - - "settings.permissions.tool.read.title": "Read", - "settings.permissions.tool.read.description": "Reading a file (matches the file path)", - "settings.permissions.tool.edit.title": "Edit", - "settings.permissions.tool.edit.description": "Modify files, including edits, writes, patches, and multi-edits", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "Match files using glob patterns", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "Search file contents using regular expressions", - "settings.permissions.tool.list.title": "List", - "settings.permissions.tool.list.description": "List files within a directory", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "Run shell commands", - "settings.permissions.tool.task.title": "Task", - "settings.permissions.tool.task.description": "Launch sub-agents", - "settings.permissions.tool.skill.title": "Skill", - "settings.permissions.tool.skill.description": "Load a skill by name", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "Run language server queries", - "settings.permissions.tool.todowrite.title": "Todo Write", - "settings.permissions.tool.todowrite.description": "Update the todo list", - "settings.permissions.tool.webfetch.title": "Web Fetch", - "settings.permissions.tool.webfetch.description": "Fetch content from a URL", - "settings.permissions.tool.websearch.title": "Web Search", - "settings.permissions.tool.websearch.description": "Search the web", - "settings.permissions.tool.codesearch.title": "Code Search", - "settings.permissions.tool.codesearch.description": "Search code on the web", - "settings.permissions.tool.external_directory.title": "External Directory", - "settings.permissions.tool.external_directory.description": "Access files outside the project directory", - "settings.permissions.tool.doom_loop.title": "Doom Loop", - "settings.permissions.tool.doom_loop.description": "Detect repeated tool calls with identical input", - - "session.delete.failed.title": "Failed to delete session", - "session.delete.title": "Delete session", - "session.delete.confirm": 'Delete session "{{name}}"?', - "session.delete.button": "Delete session", - - "workspace.new": "New workspace", - "workspace.type.local": "local", - "workspace.type.sandbox": "sandbox", - "workspace.create.failed.title": "Failed to create workspace", - "workspace.delete.failed.title": "Failed to delete workspace", - "workspace.resetting.title": "Resetting workspace", - "workspace.resetting.description": "This may take a minute.", - "workspace.reset.failed.title": "Failed to reset workspace", - "workspace.reset.success.title": "Workspace reset", - "workspace.reset.success.description": "Workspace now matches the default branch.", - "workspace.error.stillPreparing": "Workspace is still preparing", - "workspace.status.checking": "Checking for unmerged changes...", - "workspace.status.error": "Unable to verify git status.", - "workspace.status.clean": "No unmerged changes detected.", - "workspace.status.dirty": "Unmerged changes detected in this workspace.", - "workspace.delete.title": "Delete workspace", - "workspace.delete.confirm": 'Delete workspace "{{name}}"?', - "workspace.delete.button": "Delete workspace", - "workspace.reset.title": "Reset workspace", - "workspace.reset.confirm": 'Reset workspace "{{name}}"?', - "workspace.reset.button": "Reset workspace", - "workspace.reset.archived.none": "No active sessions will be archived.", - "workspace.reset.archived.one": "1 session will be archived.", - "workspace.reset.archived.many": "{{count}} sessions will be archived.", - "workspace.reset.note": "This will reset the workspace to match the default branch.", -} diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts deleted file mode 100644 index 9ed93e813e4..00000000000 --- a/packages/app/src/i18n/es.ts +++ /dev/null @@ -1,942 +0,0 @@ -export const dict = { - "command.category.suggested": "Sugerido", - "command.category.view": "Ver", - "command.category.project": "Proyecto", - "command.category.provider": "Proveedor", - "command.category.server": "Servidor", - "command.category.session": "Sesión", - "command.category.theme": "Tema", - "command.category.language": "Idioma", - "command.category.file": "Archivo", - "command.category.context": "Contexto", - "command.category.terminal": "Terminal", - "command.category.model": "Modelo", - "command.category.mcp": "MCP", - "command.category.agent": "Agente", - "command.category.permissions": "Permisos", - "command.category.workspace": "Espacio de trabajo", - "command.category.settings": "Ajustes", - - "theme.scheme.system": "Sistema", - "theme.scheme.light": "Claro", - "theme.scheme.dark": "Oscuro", - - "command.sidebar.toggle": "Alternar barra lateral", - "command.project.open": "Abrir proyecto", - "command.provider.connect": "Conectar proveedor", - "command.server.switch": "Cambiar servidor", - "command.settings.open": "Abrir ajustes", - "command.session.previous": "Sesión anterior", - "command.session.next": "Siguiente sesión", - "command.session.previous.unseen": "Sesión no leída anterior", - "command.session.next.unseen": "Siguiente sesión no leída", - "command.session.archive": "Archivar sesión", - - "command.palette": "Paleta de comandos", - - "command.theme.cycle": "Alternar tema", - "command.theme.set": "Usar tema: {{theme}}", - "command.theme.scheme.cycle": "Alternar esquema de color", - "command.theme.scheme.set": "Usar esquema de color: {{scheme}}", - - "command.language.cycle": "Alternar idioma", - "command.language.set": "Usar idioma: {{language}}", - - "command.session.new": "Nueva sesión", - "command.file.open": "Abrir archivo", - "command.tab.close": "Cerrar pestaña", - "command.context.addSelection": "Añadir selección al contexto", - "command.context.addSelection.description": "Añadir las líneas seleccionadas del archivo actual", - "command.input.focus": "Enfocar entrada", - "command.terminal.toggle": "Alternar terminal", - "command.fileTree.toggle": "Alternar árbol de archivos", - "command.review.toggle": "Alternar revisión", - "command.terminal.new": "Nueva terminal", - "command.terminal.new.description": "Crear una nueva pestaña de terminal", - "command.steps.toggle": "Alternar pasos", - "command.steps.toggle.description": "Mostrar u ocultar pasos para el mensaje actual", - "command.message.previous": "Mensaje anterior", - "command.message.previous.description": "Ir al mensaje de usuario anterior", - "command.message.next": "Siguiente mensaje", - "command.message.next.description": "Ir al siguiente mensaje de usuario", - "command.model.choose": "Elegir modelo", - "command.model.choose.description": "Seleccionar un modelo diferente", - "command.mcp.toggle": "Alternar MCPs", - "command.mcp.toggle.description": "Alternar MCPs", - "command.agent.cycle": "Alternar agente", - "command.agent.cycle.description": "Cambiar al siguiente agente", - "command.agent.cycle.reverse": "Alternar agente hacia atrás", - "command.agent.cycle.reverse.description": "Cambiar al agente anterior", - "command.model.variant.cycle": "Alternar esfuerzo de pensamiento", - "command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo", - "command.prompt.mode.shell": "Shell", - "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Aceptar permisos automáticamente", - "command.permissions.autoaccept.disable": "Dejar de aceptar permisos automáticamente", - "command.workspace.toggle": "Alternar espacios de trabajo", - "command.workspace.toggle.description": "Habilitar o deshabilitar múltiples espacios de trabajo en la barra lateral", - "command.session.undo": "Deshacer", - "command.session.undo.description": "Deshacer el último mensaje", - "command.session.redo": "Rehacer", - "command.session.redo.description": "Rehacer el último mensaje deshecho", - "command.session.compact": "Compactar sesión", - "command.session.compact.description": "Resumir la sesión para reducir el tamaño del contexto", - "command.session.fork": "Bifurcar desde mensaje", - "command.session.fork.description": "Crear una nueva sesión desde un mensaje anterior", - "command.session.share": "Compartir sesión", - "command.session.share.description": "Compartir esta sesión y copiar la URL al portapapeles", - "command.session.unshare": "Dejar de compartir sesión", - "command.session.unshare.description": "Dejar de compartir esta sesión", - - "palette.search.placeholder": "Buscar archivos, comandos y sesiones", - "palette.empty": "No se encontraron resultados", - "palette.group.commands": "Comandos", - "palette.group.files": "Archivos", - - "dialog.provider.search.placeholder": "Buscar proveedores", - "dialog.provider.empty": "No se encontraron proveedores", - "dialog.provider.group.popular": "Popular", - "dialog.provider.group.other": "Otro", - "dialog.provider.tag.recommended": "Recomendado", - "dialog.provider.opencode.note": "Modelos seleccionados incluyendo Claude, GPT, Gemini y más", - "dialog.provider.opencode.tagline": "Modelos optimizados y fiables", - "dialog.provider.opencodeGo.tagline": "Suscripción económica para todos", - "dialog.provider.anthropic.note": "Acceso directo a modelos Claude, incluyendo Pro y Max", - "dialog.provider.copilot.note": "Modelos de IA para asistencia de codificación a través de GitHub Copilot", - "dialog.provider.openai.note": "Modelos GPT para tareas de IA generales rápidas y capaces", - "dialog.provider.google.note": "Modelos Gemini para respuestas rápidas y estructuradas", - "dialog.provider.openrouter.note": "Accede a todos los modelos soportados desde un solo proveedor", - "dialog.provider.vercel.note": "Acceso unificado a modelos de IA con enrutamiento inteligente", - - "dialog.model.select.title": "Seleccionar modelo", - "dialog.model.search.placeholder": "Buscar modelos", - "dialog.model.empty": "Sin resultados de modelos", - "dialog.model.manage": "Gestionar modelos", - "dialog.model.manage.description": "Personalizar qué modelos aparecen en el selector de modelos.", - "dialog.model.manage.provider.toggle": "Alternar todos los modelos de {{provider}}", - - "dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por Kilo", - "dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares", - - "dialog.provider.viewAll": "Ver más proveedores", - - "provider.connect.title": "Conectar {{provider}}", - "provider.connect.title.anthropicProMax": "Iniciar sesión con Claude Pro/Max", - "provider.connect.selectMethod": "Seleccionar método de inicio de sesión para {{provider}}.", - "provider.connect.method.apiKey": "Clave API", - "provider.connect.status.inProgress": "Autorización en progreso...", - "provider.connect.status.waiting": "Esperando autorización...", - "provider.connect.status.failed": "Autorización fallida: {{error}}", - "provider.connect.apiKey.description": - "Introduce tu clave API de {{provider}} para conectar tu cuenta y usar modelos de {{provider}} en Kilo.", - "provider.connect.apiKey.label": "Clave API de {{provider}}", - "provider.connect.apiKey.placeholder": "Clave API", - "provider.connect.apiKey.required": "La clave API es obligatoria", - "provider.connect.opencodeZen.line1": - "OpenCode Zen te da acceso a un conjunto curado de modelos fiables optimizados para agentes de programación.", - "provider.connect.opencodeZen.line2": - "Con una sola clave API obtendrás acceso a modelos como Claude, GPT, Gemini, GLM y más.", - "provider.connect.opencodeZen.visit.prefix": "Visita ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " para obtener tu clave API.", - "provider.connect.oauth.code.visit.prefix": "Visita ", - "provider.connect.oauth.code.visit.link": "este enlace", - "provider.connect.oauth.code.visit.suffix": - " para obtener tu código de autorización para conectar tu cuenta y usar modelos de {{provider}} en Kilo.", - "provider.connect.oauth.code.label": "Código de autorización {{method}}", - "provider.connect.oauth.code.placeholder": "Código de autorización", - "provider.connect.oauth.code.required": "El código de autorización es obligatorio", - "provider.connect.oauth.code.invalid": "Código de autorización inválido", - "provider.connect.oauth.auto.visit.prefix": "Visita ", - "provider.connect.oauth.auto.visit.link": "este enlace", - "provider.connect.oauth.auto.visit.suffix": - " e introduce el código a continuación para conectar tu cuenta y usar modelos de {{provider}} en Kilo.", - "provider.connect.oauth.auto.confirmationCode": "Código de confirmación", - "provider.connect.toast.connected.title": "{{provider}} conectado", - "provider.connect.toast.connected.description": "Los modelos de {{provider}} ahora están disponibles para usar.", - - "provider.custom.title": "Proveedor personalizado", - "provider.custom.description.prefix": "Configurar un proveedor compatible con OpenAI. Ver la ", - "provider.custom.description.link": "documentación de configuración del proveedor", - "provider.custom.description.suffix": ".", - "provider.custom.field.providerID.label": "ID del proveedor", - "provider.custom.field.providerID.placeholder": "miproveedor", - "provider.custom.field.providerID.description": "Letras minúsculas, números, guiones o guiones bajos", - "provider.custom.field.name.label": "Nombre para mostrar", - "provider.custom.field.name.placeholder": "Mi Proveedor de IA", - "provider.custom.field.baseURL.label": "URL base", - "provider.custom.field.baseURL.placeholder": "https://api.miproveedor.com/v1", - "provider.custom.field.apiKey.label": "Clave API", - "provider.custom.field.apiKey.placeholder": "Clave API", - "provider.custom.field.apiKey.description": "Opcional. Dejar vacío si gestionas la autenticación mediante cabeceras.", - "provider.custom.models.label": "Modelos", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "id-modelo", - "provider.custom.models.name.label": "Nombre", - "provider.custom.models.name.placeholder": "Nombre para mostrar", - "provider.custom.models.remove": "Eliminar modelo", - "provider.custom.models.add": "Añadir modelo", - "provider.custom.headers.label": "Cabeceras (opcional)", - "provider.custom.headers.key.label": "Cabecera", - "provider.custom.headers.key.placeholder": "Nombre-Cabecera", - "provider.custom.headers.value.label": "Valor", - "provider.custom.headers.value.placeholder": "valor", - "provider.custom.headers.remove": "Eliminar cabecera", - "provider.custom.headers.add": "Añadir cabecera", - "provider.custom.error.providerID.required": "El ID del proveedor es obligatorio", - "provider.custom.error.providerID.format": "Usa letras minúsculas, números, guiones o guiones bajos", - "provider.custom.error.providerID.exists": "Ese ID de proveedor ya existe", - "provider.custom.error.name.required": "El nombre para mostrar es obligatorio", - "provider.custom.error.baseURL.required": "La URL base es obligatoria", - "provider.custom.error.baseURL.format": "Debe comenzar con http:// o https://", - "provider.custom.error.required": "Obligatorio", - "provider.custom.error.duplicate": "Duplicado", - - "provider.disconnect.toast.disconnected.title": "{{provider}} desconectado", - "provider.disconnect.toast.disconnected.description": "Los modelos de {{provider}} ya no están disponibles.", - - "model.tag.free": "Gratis", - "model.tag.latest": "Último", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "texto", - "model.input.image": "imagen", - "model.input.audio": "audio", - "model.input.video": "video", - "model.input.pdf": "pdf", - "model.tooltip.allows": "Permite: {{inputs}}", - "model.tooltip.reasoning.allowed": "Permite razonamiento", - "model.tooltip.reasoning.none": "Sin razonamiento", - "model.tooltip.context": "Límite de contexto {{limit}}", - - "common.search.placeholder": "Buscar", - "common.goBack": "Volver", - "common.goForward": "Avanzar", - "common.loading": "Cargando", - "common.loading.ellipsis": "...", - "common.cancel": "Cancelar", - "common.connect": "Conectar", - "common.disconnect": "Desconectar", - "common.continue": "Enviar", - "common.submit": "Enviar", - "common.save": "Guardar", - "common.saving": "Guardando...", - "common.default": "Predeterminado", - "common.attachment": "adjunto", - - "prompt.placeholder.shell": "Introduce comando de shell... {{example}}", - "prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"', - "prompt.placeholder.simple": "Pregunta cualquier cosa...", - "prompt.placeholder.summarizeComments": "Resumir comentarios…", - "prompt.placeholder.summarizeComment": "Resumir comentario…", - "prompt.mode.shell": "Shell", - "prompt.mode.normal": "Prompt", - "prompt.mode.shell.exit": "esc para salir", - - "prompt.example.1": "Arreglar un TODO en el código", - "prompt.example.2": "¿Cuál es el stack tecnológico de este proyecto?", - "prompt.example.3": "Arreglar pruebas rotas", - "prompt.example.4": "Explicar cómo funciona la autenticación", - "prompt.example.5": "Encontrar y arreglar vulnerabilidades de seguridad", - "prompt.example.6": "Añadir pruebas unitarias para el servicio de usuario", - "prompt.example.7": "Refactorizar esta función para que sea más legible", - "prompt.example.8": "¿Qué significa este error?", - "prompt.example.9": "Ayúdame a depurar este problema", - "prompt.example.10": "Generar documentación de API", - "prompt.example.11": "Optimizar consultas a la base de datos", - "prompt.example.12": "Añadir validación de entrada", - "prompt.example.13": "Crear un nuevo componente para...", - "prompt.example.14": "¿Cómo despliego este proyecto?", - "prompt.example.15": "Revisar mi código para mejores prácticas", - "prompt.example.16": "Añadir manejo de errores a esta función", - "prompt.example.17": "Explicar este patrón de regex", - "prompt.example.18": "Convertir esto a TypeScript", - "prompt.example.19": "Añadir logging en todo el código", - "prompt.example.20": "¿Qué dependencias están desactualizadas?", - "prompt.example.21": "Ayúdame a escribir un script de migración", - "prompt.example.22": "Implementar caché para este endpoint", - "prompt.example.23": "Añadir paginación a esta lista", - "prompt.example.24": "Crear un comando CLI para...", - "prompt.example.25": "¿Cómo funcionan las variables de entorno aquí?", - - "prompt.popover.emptyResults": "Sin resultados coincidentes", - "prompt.popover.emptyCommands": "Sin comandos coincidentes", - "prompt.dropzone.label": "Suelta imágenes, PDFs o archivos de texto aquí", - "prompt.dropzone.file.label": "Suelta para @mencionar archivo", - "prompt.slash.badge.custom": "personalizado", - "prompt.slash.badge.skill": "skill", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "activo", - "prompt.context.includeActiveFile": "Incluir archivo activo", - "prompt.context.removeActiveFile": "Eliminar archivo activo del contexto", - "prompt.context.removeFile": "Eliminar archivo del contexto", - "prompt.action.attachFile": "Adjuntar archivo", - "prompt.attachment.remove": "Eliminar adjunto", - "prompt.action.send": "Enviar", - "prompt.action.stop": "Detener", - - "prompt.toast.pasteUnsupported.title": "Adjunto no compatible", - "prompt.toast.pasteUnsupported.description": "Solo se pueden adjuntar imágenes, PDFs o archivos de texto aquí.", - "prompt.toast.modelAgentRequired.title": "Selecciona un agente y modelo", - "prompt.toast.modelAgentRequired.description": "Elige un agente y modelo antes de enviar un prompt.", - "prompt.toast.worktreeCreateFailed.title": "Fallo al crear el árbol de trabajo", - "prompt.toast.sessionCreateFailed.title": "Fallo al crear la sesión", - "prompt.toast.shellSendFailed.title": "Fallo al enviar comando de shell", - "prompt.toast.commandSendFailed.title": "Fallo al enviar comando", - "prompt.toast.promptSendFailed.title": "Fallo al enviar prompt", - "prompt.toast.promptSendFailed.description": "No se pudo recuperar la sesión", - - "dialog.mcp.title": "MCPs", - "dialog.mcp.description": "{{enabled}} de {{total}} habilitados", - "dialog.mcp.empty": "No hay MCPs configurados", - - "dialog.lsp.empty": "LSPs detectados automáticamente por tipo de archivo", - "dialog.plugins.empty": "Plugins configurados en opencode.json", - - "mcp.status.connected": "conectado", - "mcp.status.failed": "fallido", - "mcp.status.needs_auth": "necesita auth", - "mcp.status.disabled": "deshabilitado", - - "dialog.fork.empty": "No hay mensajes desde donde bifurcar", - - "dialog.directory.search.placeholder": "Buscar carpetas", - "dialog.directory.empty": "No se encontraron carpetas", - - "dialog.server.title": "Servidores", - "dialog.server.description": "Cambiar a qué servidor de Kilo se conecta esta app.", - "dialog.server.search.placeholder": "Buscar servidores", - "dialog.server.empty": "No hay servidores aún", - "dialog.server.add.title": "Añadir un servidor", - "dialog.server.add.url": "URL del servidor", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "No se pudo conectar al servidor", - "dialog.server.add.checking": "Comprobando...", - "dialog.server.add.button": "Añadir servidor", - "dialog.server.add.name": "Nombre del servidor (opcional)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "Nombre de usuario (opcional)", - "dialog.server.add.password": "Contraseña (opcional)", - "dialog.server.edit.title": "Editar servidor", - "dialog.server.default.title": "Servidor predeterminado", - "dialog.server.default.description": - "Conectar a este servidor al iniciar la app en lugar de iniciar un servidor local. Requiere reinicio.", - "dialog.server.default.none": "Ningún servidor seleccionado", - "dialog.server.default.set": "Establecer servidor actual como predeterminado", - "dialog.server.default.clear": "Limpiar", - "dialog.server.action.remove": "Eliminar servidor", - - "dialog.server.menu.edit": "Editar", - "dialog.server.menu.default": "Establecer como predeterminado", - "dialog.server.menu.defaultRemove": "Quitar predeterminado", - "dialog.server.menu.delete": "Eliminar", - "dialog.server.current": "Servidor actual", - "dialog.server.status.default": "Predeterminado", - - "dialog.project.edit.title": "Editar proyecto", - "dialog.project.edit.name": "Nombre", - "dialog.project.edit.icon": "Icono", - "dialog.project.edit.icon.alt": "Icono del proyecto", - "dialog.project.edit.icon.hint": "Haz clic o arrastra una imagen", - "dialog.project.edit.icon.recommended": "Recomendado: 128x128px", - "dialog.project.edit.color": "Color", - "dialog.project.edit.color.select": "Seleccionar color {{color}}", - "dialog.project.edit.worktree.startup": "Script de inicio del espacio de trabajo", - "dialog.project.edit.worktree.startup.description": - "Se ejecuta después de crear un nuevo espacio de trabajo (árbol de trabajo).", - "dialog.project.edit.worktree.startup.placeholder": "p. ej. bun install", - - "context.breakdown.title": "Desglose de Contexto", - "context.breakdown.note": - 'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.', - "context.breakdown.system": "Sistema", - "context.breakdown.user": "Usuario", - "context.breakdown.assistant": "Asistente", - "context.breakdown.tool": "Llamadas a herramientas", - "context.breakdown.other": "Otro", - - "context.systemPrompt.title": "Prompt del Sistema", - "context.rawMessages.title": "Mensajes en bruto", - - "context.stats.session": "Sesión", - "context.stats.messages": "Mensajes", - "context.stats.provider": "Proveedor", - "context.stats.model": "Modelo", - "context.stats.limit": "Límite de Contexto", - "context.stats.totalTokens": "Tokens Totales", - "context.stats.usage": "Uso", - "context.stats.inputTokens": "Tokens de Entrada", - "context.stats.outputTokens": "Tokens de Salida", - "context.stats.reasoningTokens": "Tokens de Razonamiento", - "context.stats.cacheTokens": "Tokens de Caché (lectura/escritura)", - "context.stats.userMessages": "Mensajes de Usuario", - "context.stats.assistantMessages": "Mensajes de Asistente", - "context.stats.totalCost": "Costo Total", - "context.stats.sessionCreated": "Sesión Creada", - "context.stats.lastActivity": "Última Actividad", - - "context.usage.tokens": "Tokens", - "context.usage.usage": "Uso", - "context.usage.cost": "Costo", - "context.usage.clickToView": "Haz clic para ver contexto", - "context.usage.view": "Ver uso del contexto", - - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - - "toast.language.title": "Idioma", - "toast.language.description": "Cambiado a {{language}}", - - "toast.theme.title": "Tema cambiado", - "toast.scheme.title": "Esquema de color", - - "toast.workspace.enabled.title": "Espacios de trabajo habilitados", - "toast.workspace.enabled.description": "Ahora se muestran varios worktrees en la barra lateral", - "toast.workspace.disabled.title": "Espacios de trabajo deshabilitados", - "toast.workspace.disabled.description": "Solo se muestra el worktree principal en la barra lateral", - - "toast.permissions.autoaccept.on.title": "Aceptando permisos automáticamente", - "toast.permissions.autoaccept.on.description": "Las solicitudes de permisos se aprobarán automáticamente", - "toast.permissions.autoaccept.off.title": "Se dejó de aceptar permisos automáticamente", - "toast.permissions.autoaccept.off.description": "Las solicitudes de permisos requerirán aprobación", - - "toast.model.none.title": "Ningún modelo seleccionado", - "toast.model.none.description": "Conecta un proveedor para resumir esta sesión", - - "toast.file.loadFailed.title": "Fallo al cargar archivo", - "toast.file.listFailed.title": "Fallo al listar archivos", - - "toast.context.noLineSelection.title": "Sin selección de líneas", - "toast.context.noLineSelection.description": "Primero selecciona un rango de líneas en una pestaña de archivo.", - - "toast.session.share.copyFailed.title": "Fallo al copiar URL al portapapeles", - "toast.session.share.success.title": "Sesión compartida", - "toast.session.share.success.description": "¡URL compartida copiada al portapapeles!", - "toast.session.share.failed.title": "Fallo al compartir sesión", - "toast.session.share.failed.description": "Ocurrió un error al compartir la sesión", - - "toast.session.unshare.success.title": "Sesión dejó de compartirse", - "toast.session.unshare.success.description": "¡La sesión dejó de compartirse exitosamente!", - "toast.session.unshare.failed.title": "Fallo al dejar de compartir sesión", - "toast.session.unshare.failed.description": "Ocurrió un error al dejar de compartir la sesión", - - "toast.session.listFailed.title": "Fallo al cargar sesiones para {{project}}", - - "toast.update.title": "Actualización disponible", - "toast.update.description": "Una nueva versión de Kilo ({{version}}) está disponible para instalar.", - "toast.update.action.installRestart": "Instalar y reiniciar", - "toast.update.action.notYet": "Todavía no", - - "error.page.title": "Algo salió mal", - "error.page.description": "Ocurrió un error al cargar la aplicación.", - "error.page.details.label": "Detalles del error", - "error.page.action.restart": "Reiniciar", - "error.page.action.checking": "Comprobando...", - "error.page.action.checkUpdates": "Buscar actualizaciones", - "error.page.action.updateTo": "Actualizar a {{version}}", - "error.page.report.prefix": "Por favor reporta este error al equipo de Kilo", - "error.page.report.discord": "en Discord", - "error.page.version": "Versión: {{version}}", - - "error.dev.rootNotFound": - "Elemento raíz no encontrado. ¿Olvidaste añadirlo a tu index.html? ¿O tal vez el atributo id está mal escrito?", - - "error.globalSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?", - "directory.error.invalidUrl": "URL de directorio inválida.", - - "error.chain.unknown": "Error desconocido", - "error.chain.causedBy": "Causado por:", - "error.chain.apiError": "Error de API", - "error.chain.status": "Estado: {{status}}", - "error.chain.retryable": "Reintentable: {{retryable}}", - "error.chain.responseBody": "Cuerpo de la respuesta:\n{{body}}", - "error.chain.didYouMean": "¿Quisiste decir: {{suggestions}}", - "error.chain.modelNotFound": "Modelo no encontrado: {{provider}}/{{model}}", - "error.chain.checkConfig": "Comprueba los nombres de proveedor/modelo en tu configuración (opencode.json)", - "error.chain.mcpFailed": 'El servidor MCP "{{name}}" falló. Nota, Kilo no soporta autenticación MCP todavía.', - "error.chain.providerAuthFailed": "Autenticación de proveedor fallida ({{provider}}): {{message}}", - "error.chain.providerInitFailed": - 'Fallo al inicializar proveedor "{{provider}}". Comprueba credenciales y configuración.', - "error.chain.configJsonInvalid": "El archivo de configuración en {{path}} no es un JSON(C) válido", - "error.chain.configJsonInvalidWithMessage": - "El archivo de configuración en {{path}} no es un JSON(C) válido: {{message}}", - "error.chain.configDirectoryTypo": - 'El directorio "{{dir}}" en {{path}} no es válido. Renombra el directorio a "{{suggestion}}" o elimínalo. Esto es un error tipográfico común.', - "error.chain.configFrontmatterError": "Fallo al analizar frontmatter en {{path}}:\n{{message}}", - "error.chain.configInvalid": "El archivo de configuración en {{path}} es inválido", - "error.chain.configInvalidWithMessage": "El archivo de configuración en {{path}} es inválido: {{message}}", - - "notification.permission.title": "Permiso requerido", - "notification.permission.description": "{{sessionTitle}} en {{projectName}} necesita permiso", - "notification.question.title": "Pregunta", - "notification.question.description": "{{sessionTitle}} en {{projectName}} tiene una pregunta", - "notification.action.goToSession": "Ir a sesión", - - "notification.session.responseReady.title": "Respuesta lista", - "notification.session.error.title": "Error de sesión", - "notification.session.error.fallbackDescription": "Ocurrió un error", - - "home.recentProjects": "Proyectos recientes", - "home.empty.title": "Sin proyectos recientes", - "home.empty.description": "Empieza abriendo un proyecto local", - - "session.tab.session": "Sesión", - "session.tab.review": "Revisión", - "session.tab.context": "Contexto", - "session.panel.reviewAndFiles": "Revisión y archivos", - "session.review.filesChanged": "{{count}} Archivos Cambiados", - "session.review.change.one": "Cambio", - "session.review.change.other": "Cambios", - "session.review.loadingChanges": "Cargando cambios...", - "session.review.empty": "No hay cambios en esta sesión aún", - "session.review.noVcs": "No se detectó Sistema de Control de Versiones Git, los cambios no se muestran", - "session.review.noSnapshot": - "El seguimiento de instantáneas está deshabilitado en la configuración, por lo que los cambios de sesión no están disponibles", - "session.review.noChanges": "Sin cambios", - - "session.files.selectToOpen": "Selecciona un archivo para abrir", - "session.files.all": "Todos los archivos", - "session.files.empty": "Sin archivos", - "session.files.binaryContent": "Archivo binario (el contenido no puede ser mostrado)", - - "session.messages.renderEarlier": "Renderizar mensajes anteriores", - "session.messages.loadingEarlier": "Cargando mensajes anteriores...", - "session.messages.loadEarlier": "Cargar mensajes anteriores", - "session.messages.loading": "Cargando mensajes...", - "session.messages.jumpToLatest": "Ir al último", - - "session.context.addToContext": "Añadir {{selection}} al contexto", - "session.todo.title": "Tareas", - "session.todo.collapse": "Contraer", - "session.todo.expand": "Expandir", - "session.followupDock.summary.one": "{{count}} mensaje en cola", - "session.followupDock.summary.other": "{{count}} mensajes en cola", - "session.followupDock.sendNow": "Enviar ahora", - "session.followupDock.edit": "Editar", - "session.followupDock.collapse": "Contraer mensajes en cola", - "session.followupDock.expand": "Expandir mensajes en cola", - "session.revertDock.summary.one": "{{count}} mensaje revertido", - "session.revertDock.summary.other": "{{count}} mensajes revertidos", - "session.revertDock.collapse": "Contraer mensajes revertidos", - "session.revertDock.expand": "Expandir mensajes revertidos", - "session.revertDock.restore": "Restaurar mensaje", - - "session.new.title": "Construye lo que quieras", - "session.new.worktree.main": "Rama principal", - "session.new.worktree.mainWithBranch": "Rama principal ({{branch}})", - "session.new.worktree.create": "Crear nuevo árbol de trabajo", - "session.new.lastModified": "Última modificación", - - "session.header.search.placeholder": "Buscar {{project}}", - "session.header.searchFiles": "Buscar archivos", - "session.header.openIn": "Abrir en", - "session.header.open.action": "Abrir {{app}}", - "session.header.open.ariaLabel": "Abrir en {{app}}", - "session.header.open.menu": "Opciones de apertura", - "session.header.open.copyPath": "Copiar ruta", - - "status.popover.trigger": "Estado", - "status.popover.ariaLabel": "Configuraciones del servidor", - "status.popover.tab.servers": "Servidores", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "Plugins", - "status.popover.action.manageServers": "Administrar servidores", - - "session.share.popover.title": "Publicar en web", - "session.share.popover.description.shared": - "Esta sesión es pública en la web. Es accesible para cualquiera con el enlace.", - "session.share.popover.description.unshared": - "Compartir sesión públicamente en la web. Será accesible para cualquiera con el enlace.", - "session.share.action.share": "Compartir", - "session.share.action.publish": "Publicar", - "session.share.action.publishing": "Publicando...", - "session.share.action.unpublish": "Despublicar", - "session.share.action.unpublishing": "Despublicando...", - "session.share.action.view": "Ver", - "session.share.copy.copied": "Copiado", - "session.share.copy.copyLink": "Copiar enlace", - - "lsp.tooltip.none": "Sin servidores LSP", - "lsp.label.connected": "{{count}} LSP", - - "prompt.loading": "Cargando prompt...", - "terminal.loading": "Cargando terminal...", - "terminal.title": "Terminal", - "terminal.title.numbered": "Terminal {{number}}", - "terminal.close": "Cerrar terminal", - "terminal.connectionLost.title": "Conexión perdida", - "terminal.connectionLost.description": - "La conexión del terminal se interrumpió. Esto puede ocurrir cuando el servidor se reinicia.", - - "common.closeTab": "Cerrar pestaña", - "common.dismiss": "Descartar", - "common.requestFailed": "Solicitud fallida", - "common.moreOptions": "Más opciones", - "common.learnMore": "Saber más", - "common.rename": "Renombrar", - "common.reset": "Restablecer", - "common.archive": "Archivar", - "common.delete": "Eliminar", - "common.close": "Cerrar", - "common.edit": "Editar", - "common.loadMore": "Cargar más", - "common.key.esc": "ESC", - - "sidebar.menu.toggle": "Alternar menú", - "sidebar.nav.projectsAndSessions": "Proyectos y sesiones", - "sidebar.settings": "Ajustes", - "sidebar.help": "Ayuda", - "sidebar.workspaces.enable": "Habilitar espacios de trabajo", - "sidebar.workspaces.disable": "Deshabilitar espacios de trabajo", - "sidebar.gettingStarted.title": "Empezando", - "sidebar.gettingStarted.line1": "Kilo incluye modelos gratuitos para que puedas empezar inmediatamente.", - "sidebar.gettingStarted.line2": "Conecta cualquier proveedor para usar modelos, inc. Claude, GPT, Gemini etc.", - "sidebar.project.recentSessions": "Sesiones recientes", - "sidebar.project.viewAllSessions": "Ver todas las sesiones", - "sidebar.project.clearNotifications": "Borrar notificaciones", - - "app.name.desktop": "Kilo Desktop", - - "settings.section.desktop": "Escritorio", - "settings.section.server": "Servidor", - "settings.tab.general": "General", - "settings.tab.shortcuts": "Atajos", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "Integración con WSL", - "settings.desktop.wsl.description": "Ejecutar el servidor Kilo dentro de WSL en Windows.", - - "settings.general.section.appearance": "Apariencia", - "settings.general.section.notifications": "Notificaciones del sistema", - "settings.general.section.updates": "Actualizaciones", - "settings.general.section.sounds": "Efectos de sonido", - "settings.general.section.feed": "Feed", - "settings.general.section.display": "Pantalla", - - "settings.general.row.language.title": "Idioma", - "settings.general.row.language.description": "Cambiar el idioma de visualización para Kilo", - "settings.general.row.appearance.title": "Apariencia", - "settings.general.row.appearance.description": "Personaliza cómo se ve Kilo en tu dispositivo", - "settings.general.row.colorScheme.title": "Esquema de color", - "settings.general.row.colorScheme.description": "Elige si Kilo sigue el tema del sistema, claro u oscuro", - "settings.general.row.theme.title": "Tema", - "settings.general.row.theme.description": "Personaliza el tema de Kilo.", - "settings.general.row.font.title": "Fuente de código", - "settings.general.row.font.description": "Personaliza la fuente usada en bloques de código", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "Fuente de la interfaz", - "settings.general.row.uiFont.description": "Personaliza la fuente usada en toda la interfaz", - "settings.general.row.followup.title": "Comportamiento de seguimiento", - "settings.general.row.followup.description": - "Elige si los prompts de seguimiento se dirigen inmediatamente o esperan en una cola", - "settings.general.row.followup.option.queue": "Cola", - "settings.general.row.followup.option.steer": "Dirigir", - "settings.general.row.reasoningSummaries.title": "Mostrar resúmenes de razonamiento", - "settings.general.row.reasoningSummaries.description": - "Mostrar resúmenes del razonamiento del modelo en la línea de tiempo", - "settings.general.row.shellToolPartsExpanded.title": "Expandir partes de la herramienta shell", - "settings.general.row.shellToolPartsExpanded.description": - "Mostrar las partes de la herramienta shell expandidas por defecto en la línea de tiempo", - "settings.general.row.editToolPartsExpanded.title": "Expandir partes de la herramienta de edición", - "settings.general.row.editToolPartsExpanded.description": - "Mostrar las partes de las herramientas de edición, escritura y parcheado expandidas por defecto en la línea de tiempo", - "settings.general.row.showSessionProgressBar.title": "Mostrar barra de progreso de la sesión", - "settings.general.row.showSessionProgressBar.description": - "Mostrar la barra de progreso animada en la parte superior de la sesión cuando el agente esté trabajando", - "settings.general.row.wayland.title": "Usar Wayland nativo", - "settings.general.row.wayland.description": "Deshabilitar fallback a X11 en Wayland. Requiere reinicio.", - "settings.general.row.wayland.tooltip": - "En Linux con monitores de frecuencia de actualización mixta, Wayland nativo puede ser más estable.", - - "settings.general.row.releaseNotes.title": "Notas de la versión", - "settings.general.row.releaseNotes.description": - 'Mostrar ventanas emergentes de "Novedades" después de las actualizaciones', - - "settings.updates.row.startup.title": "Buscar actualizaciones al iniciar", - "settings.updates.row.startup.description": "Buscar actualizaciones automáticamente cuando se inicia Kilo", - "settings.updates.row.check.title": "Buscar actualizaciones", - "settings.updates.row.check.description": "Buscar actualizaciones manualmente e instalarlas si hay alguna", - "settings.updates.action.checkNow": "Buscar ahora", - "settings.updates.action.checking": "Buscando...", - "settings.updates.toast.latest.title": "Estás al día", - "settings.updates.toast.latest.description": "Estás usando la última versión de Kilo.", - "sound.option.none": "Ninguno", - "sound.option.alert01": "Alerta 01", - "sound.option.alert02": "Alerta 02", - "sound.option.alert03": "Alerta 03", - "sound.option.alert04": "Alerta 04", - "sound.option.alert05": "Alerta 05", - "sound.option.alert06": "Alerta 06", - "sound.option.alert07": "Alerta 07", - "sound.option.alert08": "Alerta 08", - "sound.option.alert09": "Alerta 09", - "sound.option.alert10": "Alerta 10", - "sound.option.bipbop01": "Bip-bop 01", - "sound.option.bipbop02": "Bip-bop 02", - "sound.option.bipbop03": "Bip-bop 03", - "sound.option.bipbop04": "Bip-bop 04", - "sound.option.bipbop05": "Bip-bop 05", - "sound.option.bipbop06": "Bip-bop 06", - "sound.option.bipbop07": "Bip-bop 07", - "sound.option.bipbop08": "Bip-bop 08", - "sound.option.bipbop09": "Bip-bop 09", - "sound.option.bipbop10": "Bip-bop 10", - "sound.option.staplebops01": "Staplebops 01", - "sound.option.staplebops02": "Staplebops 02", - "sound.option.staplebops03": "Staplebops 03", - "sound.option.staplebops04": "Staplebops 04", - "sound.option.staplebops05": "Staplebops 05", - "sound.option.staplebops06": "Staplebops 06", - "sound.option.staplebops07": "Staplebops 07", - "sound.option.nope01": "No 01", - "sound.option.nope02": "No 02", - "sound.option.nope03": "No 03", - "sound.option.nope04": "No 04", - "sound.option.nope05": "No 05", - "sound.option.nope06": "No 06", - "sound.option.nope07": "No 07", - "sound.option.nope08": "No 08", - "sound.option.nope09": "No 09", - "sound.option.nope10": "No 10", - "sound.option.nope11": "No 11", - "sound.option.nope12": "No 12", - "sound.option.yup01": "Sí 01", - "sound.option.yup02": "Sí 02", - "sound.option.yup03": "Sí 03", - "sound.option.yup04": "Sí 04", - "sound.option.yup05": "Sí 05", - "sound.option.yup06": "Sí 06", - - "settings.general.notifications.agent.title": "Agente", - "settings.general.notifications.agent.description": - "Mostrar notificación del sistema cuando el agente termine o necesite atención", - "settings.general.notifications.permissions.title": "Permisos", - "settings.general.notifications.permissions.description": - "Mostrar notificación del sistema cuando se requiera un permiso", - "settings.general.notifications.errors.title": "Errores", - "settings.general.notifications.errors.description": "Mostrar notificación del sistema cuando ocurra un error", - - "settings.general.sounds.agent.title": "Agente", - "settings.general.sounds.agent.description": "Reproducir sonido cuando el agente termine o necesite atención", - "settings.general.sounds.permissions.title": "Permisos", - "settings.general.sounds.permissions.description": "Reproducir sonido cuando se requiera un permiso", - "settings.general.sounds.errors.title": "Errores", - "settings.general.sounds.errors.description": "Reproducir sonido cuando ocurra un error", - - "settings.shortcuts.title": "Atajos de teclado", - "settings.shortcuts.reset.button": "Restablecer a valores predeterminados", - "settings.shortcuts.reset.toast.title": "Atajos restablecidos", - "settings.shortcuts.reset.toast.description": - "Los atajos de teclado han sido restablecidos a los valores predeterminados.", - "settings.shortcuts.conflict.title": "Atajo ya en uso", - "settings.shortcuts.conflict.description": "{{keybind}} ya está asignado a {{titles}}.", - "settings.shortcuts.unassigned": "Sin asignar", - "settings.shortcuts.pressKeys": "Presiona teclas", - "settings.shortcuts.search.placeholder": "Buscar atajos", - "settings.shortcuts.search.empty": "No se encontraron atajos", - - "settings.shortcuts.group.general": "General", - "settings.shortcuts.group.session": "Sesión", - "settings.shortcuts.group.navigation": "Navegación", - "settings.shortcuts.group.modelAndAgent": "Modelo y agente", - "settings.shortcuts.group.terminal": "Terminal", - "settings.shortcuts.group.prompt": "Prompt", - - "settings.providers.title": "Proveedores", - "settings.providers.description": "La configuración de proveedores estará disponible aquí.", - "settings.providers.section.connected": "Proveedores conectados", - "settings.providers.connected.empty": "No hay proveedores conectados", - "settings.providers.section.popular": "Proveedores populares", - "settings.providers.tag.environment": "Entorno", - "settings.providers.tag.config": "Configuración", - "settings.providers.tag.custom": "Personalizado", - "settings.providers.tag.other": "Otro", - "settings.models.title": "Modelos", - "settings.models.description": "La configuración de modelos estará disponible aquí.", - "settings.agents.title": "Agentes", - "settings.agents.description": "La configuración de agentes estará disponible aquí.", - "settings.commands.title": "Comandos", - "settings.commands.description": "La configuración de comandos estará disponible aquí.", - "settings.mcp.title": "MCP", - "settings.mcp.description": "La configuración de MCP estará disponible aquí.", - - "settings.permissions.title": "Permisos", - "settings.permissions.description": "Controla qué herramientas puede usar el servidor por defecto.", - "settings.permissions.section.tools": "Herramientas", - "settings.permissions.toast.updateFailed.title": "Fallo al actualizar permisos", - - "settings.permissions.action.allow": "Permitir", - "settings.permissions.action.ask": "Preguntar", - "settings.permissions.action.deny": "Denegar", - - "settings.permissions.tool.read.title": "Leer", - "settings.permissions.tool.read.description": "Leer un archivo (coincide con la ruta del archivo)", - "settings.permissions.tool.edit.title": "Editar", - "settings.permissions.tool.edit.description": - "Modificar archivos, incluyendo ediciones, escrituras, parches y multi-ediciones", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "Coincidir archivos usando patrones glob", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "Buscar contenidos de archivo usando expresiones regulares", - "settings.permissions.tool.list.title": "Listar", - "settings.permissions.tool.list.description": "Listar archivos dentro de un directorio", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "Ejecutar comandos de shell", - "settings.permissions.tool.task.title": "Tarea", - "settings.permissions.tool.task.description": "Lanzar sub-agentes", - "settings.permissions.tool.skill.title": "Habilidad", - "settings.permissions.tool.skill.description": "Cargar una habilidad por nombre", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "Ejecutar consultas de servidor de lenguaje", - "settings.permissions.tool.todowrite.title": "Escribir Todo", - "settings.permissions.tool.todowrite.description": "Actualizar la lista de tareas", - "settings.permissions.tool.webfetch.title": "Web Fetch", - "settings.permissions.tool.webfetch.description": "Obtener contenido de una URL", - "settings.permissions.tool.websearch.title": "Búsqueda Web", - "settings.permissions.tool.websearch.description": "Buscar en la web", - "settings.permissions.tool.codesearch.title": "Búsqueda de Código", - "settings.permissions.tool.codesearch.description": "Buscar código en la web", - "settings.permissions.tool.external_directory.title": "Directorio Externo", - "settings.permissions.tool.external_directory.description": "Acceder a archivos fuera del directorio del proyecto", - "settings.permissions.tool.doom_loop.title": "Bucle Infinito", - "settings.permissions.tool.doom_loop.description": "Detectar llamadas a herramientas repetidas con entrada idéntica", - - "session.delete.failed.title": "Fallo al eliminar sesión", - "session.delete.title": "Eliminar sesión", - "session.delete.confirm": '¿Eliminar sesión "{{name}}"?', - "session.delete.button": "Eliminar sesión", - - "workspace.new": "Nuevo espacio de trabajo", - "workspace.type.local": "local", - "workspace.type.sandbox": "sandbox", - "workspace.create.failed.title": "Fallo al crear espacio de trabajo", - "workspace.delete.failed.title": "Fallo al eliminar espacio de trabajo", - "workspace.resetting.title": "Restableciendo espacio de trabajo", - "workspace.resetting.description": "Esto puede tomar un minuto.", - "workspace.reset.failed.title": "Fallo al restablecer espacio de trabajo", - "workspace.reset.success.title": "Espacio de trabajo restablecido", - "workspace.reset.success.description": "El espacio de trabajo ahora coincide con la rama predeterminada.", - "workspace.error.stillPreparing": "El espacio de trabajo aún se está preparando", - "workspace.status.checking": "Comprobando cambios no fusionados...", - "workspace.status.error": "No se pudo verificar el estado de git.", - "workspace.status.clean": "No se detectaron cambios no fusionados.", - "workspace.status.dirty": "Cambios no fusionados detectados en este espacio de trabajo.", - "workspace.delete.title": "Eliminar espacio de trabajo", - "workspace.delete.confirm": '¿Eliminar espacio de trabajo "{{name}}"?', - "workspace.delete.button": "Eliminar espacio de trabajo", - "workspace.reset.title": "Restablecer espacio de trabajo", - "workspace.reset.confirm": '¿Restablecer espacio de trabajo "{{name}}"?', - "workspace.reset.button": "Restablecer espacio de trabajo", - "workspace.reset.archived.none": "No se archivarán sesiones activas.", - "workspace.reset.archived.one": "1 sesión será archivada.", - "workspace.reset.archived.many": "{{count}} sesiones serán archivadas.", - "workspace.reset.note": "Esto restablecerá el espacio de trabajo para coincidir con la rama predeterminada.", - "common.open": "Abrir", - "dialog.releaseNotes.action.getStarted": "Comenzar", - "dialog.releaseNotes.action.next": "Siguiente", - "dialog.releaseNotes.action.hideFuture": "No mostrar esto en el futuro", - "dialog.releaseNotes.media.alt": "Vista previa de la versión", - "toast.project.reloadFailed.title": "Error al recargar {{project}}", - "error.server.invalidConfiguration": "Configuración inválida", - "common.moreCountSuffix": " (+{{count}} más)", - "common.time.justNow": "Justo ahora", - "common.time.minutesAgo.short": "hace {{count}} min", - "common.time.hoursAgo.short": "hace {{count}} h", - "common.time.daysAgo.short": "hace {{count}} d", - "settings.providers.connected.environmentDescription": "Conectado desde tus variables de entorno", - "settings.providers.custom.description": "Añade un proveedor compatible con OpenAI por su URL base.", - - "app.server.unreachable": "No se pudo conectar con {{server}}", - "app.server.retrying": "Reintentando automáticamente...", - "app.server.otherServers": "Otros servidores", - "dialog.server.add.usernamePlaceholder": "usuario", - "dialog.server.add.passwordPlaceholder": "contraseña", - "server.row.noUsername": "sin usuario", - "session.review.noVcs.createGit.title": "Crear repositorio Git", - "session.review.noVcs.createGit.description": "Rastrea, revisa y deshaz cambios en este proyecto", - "session.review.noVcs.createGit.actionLoading": "Creando repositorio Git...", - "session.review.noVcs.createGit.action": "Crear repositorio Git", - "session.todo.progress": "{{done}} de {{total}} tareas completadas", - "session.question.progress": "{{current}} de {{total}} preguntas", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "Explorador de archivos", - "session.header.open.fileManager": "Gestor de archivos", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "Terminal", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "Diagnóstico de rendimiento de desarrollo", - "debugBar.na": "n/d", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": - "Última transición de ruta completada tocando una página de sesión, medida desde el inicio del router hasta el primer pintado después de asentarse.", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "Cuadros por segundo en los últimos 5 segundos.", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "Peor tiempo de cuadro en los últimos 5 segundos.", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "Cuadros superiores a 32ms en los últimos 5 segundos.", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "Tiempo bloqueado y recuento de tareas largas en los últimos 5 segundos. Tarea máx: {{max}}.", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "Peor retraso de entrada observado en los últimos 5 segundos.", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": - "Duración aproximada de la interacción en los últimos 5 segundos. Esto es similar a INP, no el INP oficial de Web Vitals.", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "Cambio de diseño acumulativo para la vida útil actual de la aplicación.", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "Heap JS usado vs límite de heap. Solo Chromium.", - "debugBar.mem.tip": "Heap JS usado vs límite de heap. {{used}} de {{limit}}.", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Mayús", - "common.key.meta": "Meta", - "common.key.space": "Espacio", - "common.key.backspace": "Retroceso", - "common.key.enter": "Intro", - "common.key.tab": "Tab", - "common.key.delete": "Supr", - "common.key.home": "Inicio", - "common.key.end": "Fin", - "common.key.pageUp": "RePág", - "common.key.pageDown": "AvPág", - "common.key.insert": "Insert", - "common.unknown": "desconocido", - "error.page.circular": "[Circular]", - "error.globalSDK.noServerAvailable": "Ningún servidor disponible", - "error.globalSDK.serverNotAvailable": "Servidor no disponible", - "error.childStore.persistedCacheCreateFailed": "Error al crear caché persistente", - "error.childStore.persistedProjectMetadataCreateFailed": "Error al crear metadatos de proyecto persistentes", - "error.childStore.persistedProjectIconCreateFailed": "Error al crear icono de proyecto persistente", - "error.childStore.storeCreateFailed": "Error al crear almacén", - "terminal.connectionLost.abnormalClose": "WebSocket cerrado anormalmente: {{code}}", -} diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts deleted file mode 100644 index 587076d240a..00000000000 --- a/packages/app/src/i18n/fr.ts +++ /dev/null @@ -1,868 +0,0 @@ -export const dict = { - "command.category.suggested": "Suggéré", - "command.category.view": "Affichage", - "command.category.project": "Projet", - "command.category.provider": "Fournisseur", - "command.category.server": "Serveur", - "command.category.session": "Session", - "command.category.theme": "Thème", - "command.category.language": "Langue", - "command.category.file": "Fichier", - "command.category.context": "Contexte", - "command.category.terminal": "Terminal", - "command.category.model": "Modèle", - "command.category.mcp": "MCP", - "command.category.agent": "Agent", - "command.category.permissions": "Permissions", - "command.category.workspace": "Espace de travail", - "command.category.settings": "Paramètres", - "theme.scheme.system": "Système", - "theme.scheme.light": "Clair", - "theme.scheme.dark": "Sombre", - "command.sidebar.toggle": "Basculer la barre latérale", - "command.project.open": "Ouvrir un projet", - "command.provider.connect": "Connecter un fournisseur", - "command.server.switch": "Changer de serveur", - "command.settings.open": "Ouvrir les paramètres", - "command.session.previous": "Session précédente", - "command.session.next": "Session suivante", - "command.session.previous.unseen": "Session non lue précédente", - "command.session.next.unseen": "Session non lue suivante", - "command.session.archive": "Archiver la session", - "command.palette": "Palette de commandes", - "command.theme.cycle": "Changer de thème", - "command.theme.set": "Utiliser le thème : {{theme}}", - "command.theme.scheme.cycle": "Changer de schéma de couleurs", - "command.theme.scheme.set": "Utiliser le schéma de couleurs : {{scheme}}", - "command.language.cycle": "Changer de langue", - "command.language.set": "Utiliser la langue : {{language}}", - "command.session.new": "Nouvelle session", - "command.file.open": "Ouvrir un fichier", - "command.tab.close": "Fermer l'onglet", - "command.context.addSelection": "Ajouter la sélection au contexte", - "command.context.addSelection.description": "Ajouter les lignes sélectionnées du fichier actuel", - "command.input.focus": "Focus sur l'entrée", - "command.terminal.toggle": "Basculer le terminal", - "command.fileTree.toggle": "Basculer l'arborescence des fichiers", - "command.review.toggle": "Basculer la revue", - "command.terminal.new": "Nouveau terminal", - "command.terminal.new.description": "Créer un nouvel onglet de terminal", - "command.steps.toggle": "Basculer les étapes", - "command.steps.toggle.description": "Afficher ou masquer les étapes du message actuel", - "command.message.previous": "Message précédent", - "command.message.previous.description": "Aller au message utilisateur précédent", - "command.message.next": "Message suivant", - "command.message.next.description": "Aller au message utilisateur suivant", - "command.model.choose": "Choisir le modèle", - "command.model.choose.description": "Sélectionner un modèle différent", - "command.mcp.toggle": "Basculer MCP", - "command.mcp.toggle.description": "Basculer les MCPs", - "command.agent.cycle": "Changer d'agent", - "command.agent.cycle.description": "Passer à l'agent suivant", - "command.agent.cycle.reverse": "Changer d'agent (inverse)", - "command.agent.cycle.reverse.description": "Passer à l'agent précédent", - "command.model.variant.cycle": "Changer l'effort de réflexion", - "command.model.variant.cycle.description": "Passer au niveau d'effort suivant", - "command.prompt.mode.shell": "Shell", - "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Accepter automatiquement les permissions", - "command.permissions.autoaccept.disable": "Arrêter d'accepter automatiquement les permissions", - "command.workspace.toggle": "Basculer les espaces de travail", - "command.workspace.toggle.description": "Activer ou désactiver plusieurs espaces de travail dans la barre latérale", - "command.session.undo": "Annuler", - "command.session.undo.description": "Annuler le dernier message", - "command.session.redo": "Rétablir", - "command.session.redo.description": "Rétablir le dernier message annulé", - "command.session.compact": "Compacter la session", - "command.session.compact.description": "Résumer la session pour réduire la taille du contexte", - "command.session.fork": "Bifurquer à partir du message", - "command.session.fork.description": "Créer une nouvelle session à partir d'un message précédent", - "command.session.share": "Partager la session", - "command.session.share.description": "Partager cette session et copier l'URL dans le presse-papiers", - "command.session.unshare": "Ne plus partager la session", - "command.session.unshare.description": "Arrêter de partager cette session", - "palette.search.placeholder": "Rechercher des fichiers, des commandes et des sessions", - "palette.empty": "Aucun résultat trouvé", - "palette.group.commands": "Commandes", - "palette.group.files": "Fichiers", - "dialog.provider.search.placeholder": "Rechercher des fournisseurs", - "dialog.provider.empty": "Aucun fournisseur trouvé", - "dialog.provider.group.popular": "Populaire", - "dialog.provider.group.other": "Autre", - "dialog.provider.tag.recommended": "Recommandé", - "dialog.provider.opencode.note": "Modèles sélectionnés incluant Claude, GPT, Gemini et plus", - "dialog.provider.opencode.tagline": "Modèles optimisés et fiables", - "dialog.provider.opencodeGo.tagline": "Abonnement abordable pour tous", - "dialog.provider.anthropic.note": "Connectez-vous avec Claude Pro/Max ou une clé API", - "dialog.provider.copilot.note": "Connectez-vous avec Copilot ou une clé API", - "dialog.provider.openai.note": "Connectez-vous avec ChatGPT Pro/Plus ou une clé API", - "dialog.provider.google.note": "Modèles Gemini pour des réponses rapides et structurées", - "dialog.provider.openrouter.note": "Accédez à tous les modèles pris en charge depuis un seul fournisseur", - "dialog.provider.vercel.note": "Accès unifié aux modèles d'IA avec routage intelligent", - "dialog.model.select.title": "Sélectionner un modèle", - "dialog.model.search.placeholder": "Rechercher des modèles", - "dialog.model.empty": "Aucun résultat de modèle", - "dialog.model.manage": "Gérer les modèles", - "dialog.model.manage.description": "Personnalisez les modèles qui apparaissent dans le sélecteur.", - "dialog.model.manage.provider.toggle": "Basculer tous les modèles {{provider}}", - "dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par Kilo", - "dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires", - "dialog.provider.viewAll": "Voir plus de fournisseurs", - "provider.connect.title": "Connecter {{provider}}", - "provider.connect.title.anthropicProMax": "Connexion avec Claude Pro/Max", - "provider.connect.selectMethod": "Sélectionnez la méthode de connexion pour {{provider}}.", - "provider.connect.method.apiKey": "Clé API", - "provider.connect.status.inProgress": "Autorisation en cours...", - "provider.connect.status.waiting": "En attente d'autorisation...", - "provider.connect.status.failed": "Échec de l'autorisation : {{error}}", - "provider.connect.apiKey.description": - "Entrez votre clé API {{provider}} pour connecter votre compte et utiliser les modèles {{provider}} dans Kilo.", - "provider.connect.apiKey.label": "Clé API {{provider}}", - "provider.connect.apiKey.placeholder": "Clé API", - "provider.connect.apiKey.required": "La clé API est requise", - "provider.connect.opencodeZen.line1": - "OpenCode Zen vous donne accès à un ensemble sélectionné de modèles fiables et optimisés pour les agents de codage.", - "provider.connect.opencodeZen.line2": - "Avec une seule clé API, vous aurez accès à des modèles tels que Claude, GPT, Gemini, GLM et plus encore.", - "provider.connect.opencodeZen.visit.prefix": "Visitez ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " pour récupérer votre clé API.", - "provider.connect.oauth.code.visit.prefix": "Visitez ", - "provider.connect.oauth.code.visit.link": "ce lien", - "provider.connect.oauth.code.visit.suffix": - " pour récupérer votre code d'autorisation afin de connecter votre compte et utiliser les modèles {{provider}} dans Kilo.", - "provider.connect.oauth.code.label": "Code d'autorisation {{method}}", - "provider.connect.oauth.code.placeholder": "Code d'autorisation", - "provider.connect.oauth.code.required": "Le code d'autorisation est requis", - "provider.connect.oauth.code.invalid": "Code d'autorisation invalide", - "provider.connect.oauth.auto.visit.prefix": "Visitez ", - "provider.connect.oauth.auto.visit.link": "ce lien", - "provider.connect.oauth.auto.visit.suffix": - " et entrez le code ci-dessous pour connecter votre compte et utiliser les modèles {{provider}} dans Kilo.", - "provider.connect.oauth.auto.confirmationCode": "Code de confirmation", - "provider.connect.toast.connected.title": "{{provider}} connecté", - "provider.connect.toast.connected.description": "Les modèles {{provider}} sont maintenant disponibles.", - "provider.custom.title": "Fournisseur personnalisé", - "provider.custom.description.prefix": "Configurer un fournisseur compatible OpenAI. Voir la ", - "provider.custom.description.link": "doc de config fournisseur", - "provider.custom.description.suffix": ".", - "provider.custom.field.providerID.label": "ID du fournisseur", - "provider.custom.field.providerID.placeholder": "monfournisseur", - "provider.custom.field.providerID.description": "Lettres minuscules, chiffres, traits d'union ou tirets bas", - "provider.custom.field.name.label": "Nom d'affichage", - "provider.custom.field.name.placeholder": "Mon fournisseur IA", - "provider.custom.field.baseURL.label": "URL de base", - "provider.custom.field.baseURL.placeholder": "https://api.monfournisseur.com/v1", - "provider.custom.field.apiKey.label": "Clé API", - "provider.custom.field.apiKey.placeholder": "Clé API", - "provider.custom.field.apiKey.description": "Optionnel. Laisser vide si vous gérez l'auth via les en-têtes.", - "provider.custom.models.label": "Modèles", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "id-modele", - "provider.custom.models.name.label": "Nom", - "provider.custom.models.name.placeholder": "Nom d'affichage", - "provider.custom.models.remove": "Supprimer le modèle", - "provider.custom.models.add": "Ajouter un modèle", - "provider.custom.headers.label": "En-têtes (optionnel)", - "provider.custom.headers.key.label": "En-tête", - "provider.custom.headers.key.placeholder": "Nom-En-Tête", - "provider.custom.headers.value.label": "Valeur", - "provider.custom.headers.value.placeholder": "valeur", - "provider.custom.headers.remove": "Supprimer l'en-tête", - "provider.custom.headers.add": "Ajouter un en-tête", - "provider.custom.error.providerID.required": "L'ID du fournisseur est requis", - "provider.custom.error.providerID.format": "Utilisez des lettres minuscules, chiffres, traits d'union ou tirets bas", - "provider.custom.error.providerID.exists": "Cet ID de fournisseur existe déjà", - "provider.custom.error.name.required": "Le nom d'affichage est requis", - "provider.custom.error.baseURL.required": "L'URL de base est requise", - "provider.custom.error.baseURL.format": "Doit commencer par http:// ou https://", - "provider.custom.error.required": "Requis", - "provider.custom.error.duplicate": "Doublon", - "provider.disconnect.toast.disconnected.title": "{{provider}} déconnecté", - "provider.disconnect.toast.disconnected.description": "Les modèles {{provider}} ne sont plus disponibles.", - "model.tag.free": "Gratuit", - "model.tag.latest": "Dernier", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "texte", - "model.input.image": "image", - "model.input.audio": "audio", - "model.input.video": "vidéo", - "model.input.pdf": "pdf", - "model.tooltip.allows": "Autorise : {{inputs}}", - "model.tooltip.reasoning.allowed": "Autorise le raisonnement", - "model.tooltip.reasoning.none": "Sans raisonnement", - "model.tooltip.context": "Limite de contexte {{limit}}", - "common.search.placeholder": "Rechercher", - "common.goBack": "Retour", - "common.goForward": "Avancer", - "common.loading": "Chargement", - "common.loading.ellipsis": "...", - "common.cancel": "Annuler", - "common.connect": "Connecter", - "common.disconnect": "Déconnecter", - "common.continue": "Soumettre", - "common.submit": "Soumettre", - "common.save": "Enregistrer", - "common.saving": "Enregistrement...", - "common.default": "Défaut", - "common.attachment": "pièce jointe", - "prompt.placeholder.shell": "Entrez une commande shell... {{example}}", - "prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"', - "prompt.placeholder.simple": "Demandez n'importe quoi...", - "prompt.placeholder.summarizeComments": "Résumer les commentaires…", - "prompt.placeholder.summarizeComment": "Résumer le commentaire…", - "prompt.mode.shell": "Shell", - "prompt.mode.normal": "Prompt", - "prompt.mode.shell.exit": "esc pour quitter", - "prompt.example.1": "Corriger un TODO dans la base de code", - "prompt.example.2": "Quelle est la pile technique de ce projet ?", - "prompt.example.3": "Réparer les tests échoués", - "prompt.example.4": "Expliquer comment fonctionne l'authentification", - "prompt.example.5": "Trouver et corriger les vulnérabilités de sécurité", - "prompt.example.6": "Ajouter des tests unitaires pour le service utilisateur", - "prompt.example.7": "Refactoriser cette fonction pour être plus lisible", - "prompt.example.8": "Que signifie cette erreur ?", - "prompt.example.9": "Aidez-moi à déboguer ce problème", - "prompt.example.10": "Générer la documentation de l'API", - "prompt.example.11": "Optimiser les requêtes de base de données", - "prompt.example.12": "Ajouter une validation d'entrée", - "prompt.example.13": "Créer un nouveau composant pour...", - "prompt.example.14": "Comment déployer ce projet ?", - "prompt.example.15": "Vérifier mon code pour les meilleures pratiques", - "prompt.example.16": "Ajouter la gestion des erreurs à cette fonction", - "prompt.example.17": "Expliquer ce modèle regex", - "prompt.example.18": "Convertir ceci en TypeScript", - "prompt.example.19": "Ajouter des logs dans toute la base de code", - "prompt.example.20": "Quelles dépendances sont obsolètes ?", - "prompt.example.21": "Aidez-moi à écrire un script de migration", - "prompt.example.22": "Implémenter la mise en cache pour ce point de terminaison", - "prompt.example.23": "Ajouter la pagination à cette liste", - "prompt.example.24": "Créer une commande CLI pour...", - "prompt.example.25": "Comment fonctionnent les variables d'environnement ici ?", - "prompt.popover.emptyResults": "Aucun résultat correspondant", - "prompt.popover.emptyCommands": "Aucune commande correspondante", - "prompt.dropzone.label": "Déposez des images, des PDF ou des fichiers texte ici", - "prompt.dropzone.file.label": "Déposez pour @mentionner le fichier", - "prompt.slash.badge.custom": "personnalisé", - "prompt.slash.badge.skill": "skill", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "actif", - "prompt.context.includeActiveFile": "Inclure le fichier actif", - "prompt.context.removeActiveFile": "Retirer le fichier actif du contexte", - "prompt.context.removeFile": "Retirer le fichier du contexte", - "prompt.action.attachFile": "Joindre un fichier", - "prompt.attachment.remove": "Supprimer la pièce jointe", - "prompt.action.send": "Envoyer", - "prompt.action.stop": "Arrêter", - "prompt.toast.pasteUnsupported.title": "Pièce jointe non prise en charge", - "prompt.toast.pasteUnsupported.description": - "Seules les images, les PDF ou les fichiers texte peuvent être joints ici.", - "prompt.toast.modelAgentRequired.title": "Sélectionnez un agent et un modèle", - "prompt.toast.modelAgentRequired.description": "Choisissez un agent et un modèle avant d'envoyer un message.", - "prompt.toast.worktreeCreateFailed.title": "Échec de la création de l'arbre de travail", - "prompt.toast.sessionCreateFailed.title": "Échec de la création de la session", - "prompt.toast.shellSendFailed.title": "Échec de l'envoi de la commande shell", - "prompt.toast.commandSendFailed.title": "Échec de l'envoi de la commande", - "prompt.toast.promptSendFailed.title": "Échec de l'envoi du message", - "prompt.toast.promptSendFailed.description": "Impossible de récupérer la session", - "dialog.mcp.title": "MCPs", - "dialog.mcp.description": "{{enabled}} sur {{total}} activés", - "dialog.mcp.empty": "Aucun MCP configuré", - "dialog.lsp.empty": "LSPs détectés automatiquement par type de fichier", - "dialog.plugins.empty": "Plugins configurés dans opencode.json", - "mcp.status.connected": "connecté", - "mcp.status.failed": "échoué", - "mcp.status.needs_auth": "nécessite auth", - "mcp.status.disabled": "désactivé", - "dialog.fork.empty": "Aucun message à partir duquel bifurquer", - "dialog.directory.search.placeholder": "Rechercher des dossiers", - "dialog.directory.empty": "Aucun dossier trouvé", - "dialog.server.title": "Serveurs", - "dialog.server.description": "Changez le serveur Kilo auquel cette application se connecte.", - "dialog.server.search.placeholder": "Rechercher des serveurs", - "dialog.server.empty": "Aucun serveur pour l'instant", - "dialog.server.add.title": "Ajouter un serveur", - "dialog.server.add.url": "URL du serveur", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "Impossible de se connecter au serveur", - "dialog.server.add.checking": "Vérification...", - "dialog.server.add.button": "Ajouter un serveur", - "dialog.server.add.name": "Nom du serveur (optionnel)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "Nom d'utilisateur (optionnel)", - "dialog.server.add.password": "Mot de passe (optionnel)", - "dialog.server.edit.title": "Modifier le serveur", - "dialog.server.default.title": "Serveur par défaut", - "dialog.server.default.description": - "Se connecter à ce serveur au lancement de l'application au lieu de démarrer un serveur local. Nécessite un redémarrage.", - "dialog.server.default.none": "Aucun serveur sélectionné", - "dialog.server.default.set": "Définir le serveur actuel comme défaut", - "dialog.server.default.clear": "Effacer", - "dialog.server.action.remove": "Supprimer le serveur", - "dialog.server.menu.edit": "Modifier", - "dialog.server.menu.default": "Définir par défaut", - "dialog.server.menu.defaultRemove": "Supprimer par défaut", - "dialog.server.menu.delete": "Supprimer", - "dialog.server.current": "Serveur actuel", - "dialog.server.status.default": "Défaut", - "dialog.project.edit.title": "Modifier le projet", - "dialog.project.edit.name": "Nom", - "dialog.project.edit.icon": "Icône", - "dialog.project.edit.icon.alt": "Icône du projet", - "dialog.project.edit.icon.hint": "Cliquez ou faites glisser une image", - "dialog.project.edit.icon.recommended": "Recommandé : 128x128px", - "dialog.project.edit.color": "Couleur", - "dialog.project.edit.color.select": "Sélectionner la couleur {{color}}", - "dialog.project.edit.worktree.startup": "Script de démarrage de l'espace de travail", - "dialog.project.edit.worktree.startup.description": - "S'exécute après la création d'un nouvel espace de travail (arbre de travail).", - "dialog.project.edit.worktree.startup.placeholder": "p. ex. bun install", - "context.breakdown.title": "Répartition du contexte", - "context.breakdown.note": - "Répartition approximative des jetons d'entrée. \"Autre\" inclut les définitions d'outils et les frais généraux.", - "context.breakdown.system": "Système", - "context.breakdown.user": "Utilisateur", - "context.breakdown.assistant": "Assistant", - "context.breakdown.tool": "Appels d'outils", - "context.breakdown.other": "Autre", - "context.systemPrompt.title": "Prompt système", - "context.rawMessages.title": "Messages bruts", - "context.stats.session": "Session", - "context.stats.messages": "Messages", - "context.stats.provider": "Fournisseur", - "context.stats.model": "Modèle", - "context.stats.limit": "Limite de contexte", - "context.stats.totalTokens": "Total des jetons", - "context.stats.usage": "Utilisation", - "context.stats.inputTokens": "Jetons d'entrée", - "context.stats.outputTokens": "Jetons de sortie", - "context.stats.reasoningTokens": "Jetons de raisonnement", - "context.stats.cacheTokens": "Jetons de cache (lecture/écriture)", - "context.stats.userMessages": "Messages utilisateur", - "context.stats.assistantMessages": "Messages assistant", - "context.stats.totalCost": "Coût total", - "context.stats.sessionCreated": "Session créée", - "context.stats.lastActivity": "Dernière activité", - "context.usage.tokens": "Jetons", - "context.usage.usage": "Utilisation", - "context.usage.cost": "Coût", - "context.usage.clickToView": "Cliquez pour voir le contexte", - "context.usage.view": "Voir l'utilisation du contexte", - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - "toast.language.title": "Langue", - "toast.language.description": "Passé à {{language}}", - "toast.theme.title": "Thème changé", - "toast.scheme.title": "Schéma de couleurs", - "toast.workspace.enabled.title": "Espaces de travail activés", - "toast.workspace.enabled.description": "Plusieurs worktrees sont désormais affichés dans la barre latérale", - "toast.workspace.disabled.title": "Espaces de travail désactivés", - "toast.workspace.disabled.description": "Seul le worktree principal est affiché dans la barre latérale", - "toast.permissions.autoaccept.on.title": "Acceptation automatique des permissions", - "toast.permissions.autoaccept.on.description": "Les demandes de permission seront approuvées automatiquement", - "toast.permissions.autoaccept.off.title": "Acceptation automatique des permissions arrêtée", - "toast.permissions.autoaccept.off.description": "Les demandes de permission nécessiteront une approbation", - "toast.model.none.title": "Aucun modèle sélectionné", - "toast.model.none.description": "Connectez un fournisseur pour résumer cette session", - "toast.file.loadFailed.title": "Échec du chargement du fichier", - "toast.file.listFailed.title": "Échec de la liste des fichiers", - "toast.context.noLineSelection.title": "Aucune sélection de lignes", - "toast.context.noLineSelection.description": "Sélectionnez d'abord une plage de lignes dans un onglet de fichier.", - "toast.session.share.copyFailed.title": "Échec de la copie de l'URL dans le presse-papiers", - "toast.session.share.success.title": "Session partagée", - "toast.session.share.success.description": "URL de partage copiée dans le presse-papiers !", - "toast.session.share.failed.title": "Échec du partage de la session", - "toast.session.share.failed.description": "Une erreur s'est produite lors du partage de la session", - "toast.session.unshare.success.title": "Session non partagée", - "toast.session.unshare.success.description": "Session non partagée avec succès !", - "toast.session.unshare.failed.title": "Échec de l'annulation du partage", - "toast.session.unshare.failed.description": "Une erreur s'est produite lors de l'annulation du partage de la session", - "toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}", - "toast.update.title": "Mise à jour disponible", - "toast.update.description": "Une nouvelle version d'Kilo ({{version}}) est maintenant disponible pour installation.", - "toast.update.action.installRestart": "Installer et redémarrer", - "toast.update.action.notYet": "Pas encore", - "error.page.title": "Quelque chose s'est mal passé", - "error.page.description": "Une erreur s'est produite lors du chargement de l'application.", - "error.page.details.label": "Détails de l'erreur", - "error.page.action.restart": "Redémarrer", - "error.page.action.checking": "Vérification...", - "error.page.action.checkUpdates": "Vérifier les mises à jour", - "error.page.action.updateTo": "Mettre à jour vers {{version}}", - "error.page.report.prefix": "Veuillez signaler cette erreur à l'équipe Kilo", - "error.page.report.discord": "sur Discord", - "error.page.version": "Version : {{version}}", - "error.dev.rootNotFound": - "Élément racine introuvable. Avez-vous oublié de l'ajouter à votre index.html ? Ou peut-être que l'attribut id est mal orthographié ?", - "error.globalSync.connectFailed": - "Impossible de se connecter au serveur. Y a-t-il un serveur en cours d'exécution à `{{url}}` ?", - "directory.error.invalidUrl": "Répertoire invalide dans l'URL.", - "error.chain.unknown": "Erreur inconnue", - "error.chain.causedBy": "Causé par :", - "error.chain.apiError": "Erreur API", - "error.chain.status": "Statut : {{status}}", - "error.chain.retryable": "Réessayable : {{retryable}}", - "error.chain.responseBody": "Corps de la réponse :\n{{body}}", - "error.chain.didYouMean": "Vouliez-vous dire : {{suggestions}}", - "error.chain.modelNotFound": "Modèle introuvable : {{provider}}/{{model}}", - "error.chain.checkConfig": "Vérifiez votre configuration (opencode.json) pour les noms de fournisseur/modèle", - "error.chain.mcpFailed": - "Le serveur MCP \"{{name}}\" a échoué. Notez qu'Kilo ne supporte pas encore l'authentification MCP.", - "error.chain.providerAuthFailed": "Échec de l'authentification du fournisseur ({{provider}}) : {{message}}", - "error.chain.providerInitFailed": - 'Échec de l\'initialisation du fournisseur "{{provider}}". Vérifiez les identifiants et la configuration.', - "error.chain.configJsonInvalid": "Le fichier de configuration à {{path}} n'est pas un JSON(C) valide", - "error.chain.configJsonInvalidWithMessage": - "Le fichier de configuration à {{path}} n'est pas un JSON(C) valide : {{message}}", - "error.chain.configDirectoryTypo": - 'Le répertoire "{{dir}}" dans {{path}} n\'est pas valide. Renommez le répertoire en "{{suggestion}}" ou supprimez-le. C\'est une faute de frappe courante.', - "error.chain.configFrontmatterError": "Échec de l'analyse du frontmatter dans {{path}} :\n{{message}}", - "error.chain.configInvalid": "Le fichier de configuration à {{path}} est invalide", - "error.chain.configInvalidWithMessage": "Le fichier de configuration à {{path}} est invalide : {{message}}", - "notification.permission.title": "Permission requise", - "notification.permission.description": "{{sessionTitle}} dans {{projectName}} a besoin d'une permission", - "notification.question.title": "Question", - "notification.question.description": "{{sessionTitle}} dans {{projectName}} a une question", - "notification.action.goToSession": "Aller à la session", - "notification.session.responseReady.title": "Réponse prête", - "notification.session.error.title": "Erreur de session", - "notification.session.error.fallbackDescription": "Une erreur s'est produite", - "home.recentProjects": "Projets récents", - "home.empty.title": "Aucun projet récent", - "home.empty.description": "Commencez par ouvrir un projet local", - "session.tab.session": "Session", - "session.tab.review": "Revue", - "session.tab.context": "Contexte", - "session.panel.reviewAndFiles": "Revue et fichiers", - "session.review.filesChanged": "{{count}} fichiers modifiés", - "session.review.change.one": "Modification", - "session.review.change.other": "Modifications", - "session.review.loadingChanges": "Chargement des modifications...", - "session.review.empty": "Aucune modification dans cette session pour l'instant", - "session.review.noChanges": "Aucune modification", - "session.review.noVcs": "Aucun système de contrôle de version Git détecté, modifications non affichées", - "session.review.noSnapshot": - "Le suivi des instantanés est désactivé dans la configuration, les modifications de session sont donc indisponibles", - "session.files.selectToOpen": "Sélectionnez un fichier à ouvrir", - "session.files.all": "Tous les fichiers", - "session.files.empty": "Aucun fichier", - "session.files.binaryContent": "Fichier binaire (le contenu ne peut pas être affiché)", - "session.messages.renderEarlier": "Afficher les messages précédents", - "session.messages.loadingEarlier": "Chargement des messages précédents...", - "session.messages.loadEarlier": "Charger les messages précédents", - "session.messages.loading": "Chargement des messages...", - "session.messages.jumpToLatest": "Aller au dernier", - "session.context.addToContext": "Ajouter {{selection}} au contexte", - "session.todo.title": "Tâches", - "session.todo.collapse": "Réduire", - "session.todo.expand": "Développer", - "session.followupDock.summary.one": "{{count}} message en file d'attente", - "session.followupDock.summary.other": "{{count}} messages en file d'attente", - "session.followupDock.sendNow": "Envoyer maintenant", - "session.followupDock.edit": "Modifier", - "session.followupDock.collapse": "Réduire les messages en file d'attente", - "session.followupDock.expand": "Développer les messages en file d'attente", - "session.revertDock.summary.one": "{{count}} message annulé", - "session.revertDock.summary.other": "{{count}} messages annulés", - "session.revertDock.collapse": "Réduire les messages annulés", - "session.revertDock.expand": "Développer les messages annulés", - "session.revertDock.restore": "Restaurer le message", - "session.new.title": "Créez ce que vous voulez", - "session.new.worktree.main": "Branche principale", - "session.new.worktree.mainWithBranch": "Branche principale ({{branch}})", - "session.new.worktree.create": "Créer un nouvel arbre de travail", - "session.new.lastModified": "Dernière modification", - "session.header.search.placeholder": "Rechercher {{project}}", - "session.header.searchFiles": "Rechercher des fichiers", - "session.header.openIn": "Ouvrir dans", - "session.header.open.action": "Ouvrir {{app}}", - "session.header.open.ariaLabel": "Ouvrir dans {{app}}", - "session.header.open.menu": "Options d'ouverture", - "session.header.open.copyPath": "Copier le chemin", - "status.popover.trigger": "Statut", - "status.popover.ariaLabel": "Configurations des serveurs", - "status.popover.tab.servers": "Serveurs", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "Plugins", - "status.popover.action.manageServers": "Gérer les serveurs", - "session.share.popover.title": "Publier sur le web", - "session.share.popover.description.shared": - "Cette session est publique sur le web. Elle est accessible à toute personne disposant du lien.", - "session.share.popover.description.unshared": - "Partager la session publiquement sur le web. Elle sera accessible à toute personne disposant du lien.", - "session.share.action.share": "Partager", - "session.share.action.publish": "Publier", - "session.share.action.publishing": "Publication...", - "session.share.action.unpublish": "Dépublier", - "session.share.action.unpublishing": "Dépublication...", - "session.share.action.view": "Voir", - "session.share.copy.copied": "Copié", - "session.share.copy.copyLink": "Copier le lien", - "lsp.tooltip.none": "Aucun serveur LSP", - "lsp.label.connected": "{{count}} LSP", - "prompt.loading": "Chargement du prompt...", - "terminal.loading": "Chargement du terminal...", - "terminal.title": "Terminal", - "terminal.title.numbered": "Terminal {{number}}", - "terminal.close": "Fermer le terminal", - "terminal.connectionLost.title": "Connexion perdue", - "terminal.connectionLost.description": - "La connexion au terminal a été interrompue. Cela peut arriver lorsque le serveur redémarre.", - "common.closeTab": "Fermer l'onglet", - "common.dismiss": "Ignorer", - "common.requestFailed": "La demande a échoué", - "common.moreOptions": "Plus d'options", - "common.learnMore": "En savoir plus", - "common.rename": "Renommer", - "common.reset": "Réinitialiser", - "common.archive": "Archiver", - "common.delete": "Supprimer", - "common.close": "Fermer", - "common.edit": "Modifier", - "common.loadMore": "Charger plus", - "common.key.esc": "ESC", - "sidebar.menu.toggle": "Basculer le menu", - "sidebar.nav.projectsAndSessions": "Projets et sessions", - "sidebar.settings": "Paramètres", - "sidebar.help": "Aide", - "sidebar.workspaces.enable": "Activer les espaces de travail", - "sidebar.workspaces.disable": "Désactiver les espaces de travail", - "sidebar.gettingStarted.title": "Commencer", - "sidebar.gettingStarted.line1": "Kilo inclut des modèles gratuits pour que vous puissiez commencer immédiatement.", - "sidebar.gettingStarted.line2": - "Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.", - "sidebar.project.recentSessions": "Sessions récentes", - "sidebar.project.viewAllSessions": "Voir toutes les sessions", - "sidebar.project.clearNotifications": "Effacer les notifications", - "app.name.desktop": "Kilo Desktop", - "settings.section.desktop": "Bureau", - "settings.section.server": "Serveur", - "settings.tab.general": "Général", - "settings.tab.shortcuts": "Raccourcis", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "Intégration WSL", - "settings.desktop.wsl.description": "Exécuter le serveur Kilo dans WSL sur Windows.", - "settings.general.section.appearance": "Apparence", - "settings.general.section.notifications": "Notifications système", - "settings.general.section.updates": "Mises à jour", - "settings.general.section.sounds": "Effets sonores", - "settings.general.section.feed": "Flux", - "settings.general.section.display": "Affichage", - "settings.general.row.language.title": "Langue", - "settings.general.row.language.description": "Changer la langue d'affichage pour Kilo", - "settings.general.row.appearance.title": "Apparence", - "settings.general.row.appearance.description": "Personnaliser l'apparence d'Kilo sur votre appareil", - "settings.general.row.colorScheme.title": "Schéma de couleurs", - "settings.general.row.colorScheme.description": "Choisissez si Kilo suit le thème système, clair ou sombre", - "settings.general.row.theme.title": "Thème", - "settings.general.row.theme.description": "Personnaliser le thème d'Kilo.", - "settings.general.row.font.title": "Police de code", - "settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "Police de l'interface", - "settings.general.row.uiFont.description": "Personnaliser la police utilisée dans toute l'interface", - "settings.general.row.followup.title": "Comportement de suivi", - "settings.general.row.followup.description": - "Choisissez si les messages de suivi dirigent immédiatement ou attendent dans une file d'attente", - "settings.general.row.followup.option.queue": "File d'attente", - "settings.general.row.followup.option.steer": "Diriger", - "settings.general.row.reasoningSummaries.title": "Afficher les résumés de raisonnement", - "settings.general.row.reasoningSummaries.description": - "Afficher les résumés de raisonnement du modèle dans la chronologie", - "settings.general.row.shellToolPartsExpanded.title": "Développer les parties de l'outil shell", - "settings.general.row.shellToolPartsExpanded.description": - "Afficher les parties de l'outil shell développées par défaut dans la chronologie", - "settings.general.row.editToolPartsExpanded.title": "Développer les parties de l'outil edit", - "settings.general.row.editToolPartsExpanded.description": - "Afficher les parties des outils edit, write et patch développées par défaut dans la chronologie", - "settings.general.row.showSessionProgressBar.title": "Afficher la barre de progression de la session", - "settings.general.row.showSessionProgressBar.description": - "Afficher la barre de progression animée en haut de la session lorsque l'agent travaille", - "settings.general.row.wayland.title": "Utiliser Wayland natif", - "settings.general.row.wayland.description": "Désactiver le repli X11 sur Wayland. Nécessite un redémarrage.", - "settings.general.row.wayland.tooltip": - "Sur Linux avec des moniteurs à taux de rafraîchissement mixte, Wayland natif peut être plus stable.", - "settings.general.row.releaseNotes.title": "Notes de version", - "settings.general.row.releaseNotes.description": 'Afficher des pop-ups "Quoi de neuf" après les mises à jour', - "settings.updates.row.startup.title": "Vérifier les mises à jour au démarrage", - "settings.updates.row.startup.description": "Vérifier automatiquement les mises à jour au lancement d'Kilo", - "settings.updates.row.check.title": "Vérifier les mises à jour", - "settings.updates.row.check.description": "Vérifier manuellement les mises à jour et installer si disponible", - "settings.updates.action.checkNow": "Vérifier maintenant", - "settings.updates.action.checking": "Vérification...", - "settings.updates.toast.latest.title": "Vous êtes à jour", - "settings.updates.toast.latest.description": "Vous utilisez la dernière version d'Kilo.", - "sound.option.none": "Aucun", - "sound.option.alert01": "Alerte 01", - "sound.option.alert02": "Alerte 02", - "sound.option.alert03": "Alerte 03", - "sound.option.alert04": "Alerte 04", - "sound.option.alert05": "Alerte 05", - "sound.option.alert06": "Alerte 06", - "sound.option.alert07": "Alerte 07", - "sound.option.alert08": "Alerte 08", - "sound.option.alert09": "Alerte 09", - "sound.option.alert10": "Alerte 10", - "sound.option.bipbop01": "Bip-bop 01", - "sound.option.bipbop02": "Bip-bop 02", - "sound.option.bipbop03": "Bip-bop 03", - "sound.option.bipbop04": "Bip-bop 04", - "sound.option.bipbop05": "Bip-bop 05", - "sound.option.bipbop06": "Bip-bop 06", - "sound.option.bipbop07": "Bip-bop 07", - "sound.option.bipbop08": "Bip-bop 08", - "sound.option.bipbop09": "Bip-bop 09", - "sound.option.bipbop10": "Bip-bop 10", - "sound.option.staplebops01": "Staplebops 01", - "sound.option.staplebops02": "Staplebops 02", - "sound.option.staplebops03": "Staplebops 03", - "sound.option.staplebops04": "Staplebops 04", - "sound.option.staplebops05": "Staplebops 05", - "sound.option.staplebops06": "Staplebops 06", - "sound.option.staplebops07": "Staplebops 07", - "sound.option.nope01": "Non 01", - "sound.option.nope02": "Non 02", - "sound.option.nope03": "Non 03", - "sound.option.nope04": "Non 04", - "sound.option.nope05": "Non 05", - "sound.option.nope06": "Non 06", - "sound.option.nope07": "Non 07", - "sound.option.nope08": "Non 08", - "sound.option.nope09": "Non 09", - "sound.option.nope10": "Non 10", - "sound.option.nope11": "Non 11", - "sound.option.nope12": "Non 12", - "sound.option.yup01": "Oui 01", - "sound.option.yup02": "Oui 02", - "sound.option.yup03": "Oui 03", - "sound.option.yup04": "Oui 04", - "sound.option.yup05": "Oui 05", - "sound.option.yup06": "Oui 06", - "settings.general.notifications.agent.title": "Agent", - "settings.general.notifications.agent.description": - "Afficher une notification système lorsque l'agent a terminé ou nécessite une attention", - "settings.general.notifications.permissions.title": "Permissions", - "settings.general.notifications.permissions.description": - "Afficher une notification système lorsqu'une permission est requise", - "settings.general.notifications.errors.title": "Erreurs", - "settings.general.notifications.errors.description": "Afficher une notification système lorsqu'une erreur se produit", - "settings.general.sounds.agent.title": "Agent", - "settings.general.sounds.agent.description": "Jouer un son lorsque l'agent a terminé ou nécessite une attention", - "settings.general.sounds.permissions.title": "Permissions", - "settings.general.sounds.permissions.description": "Jouer un son lorsqu'une permission est requise", - "settings.general.sounds.errors.title": "Erreurs", - "settings.general.sounds.errors.description": "Jouer un son lorsqu'une erreur se produit", - "settings.shortcuts.title": "Raccourcis clavier", - "settings.shortcuts.reset.button": "Rétablir les défauts", - "settings.shortcuts.reset.toast.title": "Raccourcis réinitialisés", - "settings.shortcuts.reset.toast.description": "Les raccourcis clavier ont été réinitialisés aux valeurs par défaut.", - "settings.shortcuts.conflict.title": "Raccourci déjà utilisé", - "settings.shortcuts.conflict.description": "{{keybind}} est déjà assigné à {{titles}}.", - "settings.shortcuts.unassigned": "Non assigné", - "settings.shortcuts.pressKeys": "Appuyez sur les touches", - "settings.shortcuts.search.placeholder": "Rechercher des raccourcis", - "settings.shortcuts.search.empty": "Aucun raccourci trouvé", - "settings.shortcuts.group.general": "Général", - "settings.shortcuts.group.session": "Session", - "settings.shortcuts.group.navigation": "Navigation", - "settings.shortcuts.group.modelAndAgent": "Modèle et agent", - "settings.shortcuts.group.terminal": "Terminal", - "settings.shortcuts.group.prompt": "Prompt", - "settings.providers.title": "Fournisseurs", - "settings.providers.description": "Les paramètres des fournisseurs seront configurables ici.", - "settings.providers.section.connected": "Fournisseurs connectés", - "settings.providers.connected.empty": "Aucun fournisseur connecté", - "settings.providers.section.popular": "Fournisseurs populaires", - "settings.providers.tag.environment": "Environnement", - "settings.providers.tag.config": "Configuration", - "settings.providers.tag.custom": "Personnalisé", - "settings.providers.tag.other": "Autre", - "settings.models.title": "Modèles", - "settings.models.description": "Les paramètres des modèles seront configurables ici.", - "settings.agents.title": "Agents", - "settings.agents.description": "Les paramètres des agents seront configurables ici.", - "settings.commands.title": "Commandes", - "settings.commands.description": "Les paramètres des commandes seront configurables ici.", - "settings.mcp.title": "MCP", - "settings.mcp.description": "Les paramètres MCP seront configurables ici.", - "settings.permissions.title": "Permissions", - "settings.permissions.description": "Contrôlez les outils que le serveur peut utiliser par défaut.", - "settings.permissions.section.tools": "Outils", - "settings.permissions.toast.updateFailed.title": "Échec de la mise à jour des permissions", - "settings.permissions.action.allow": "Autoriser", - "settings.permissions.action.ask": "Demander", - "settings.permissions.action.deny": "Refuser", - "settings.permissions.tool.read.title": "Lire", - "settings.permissions.tool.read.description": "Lecture d'un fichier (correspond au chemin du fichier)", - "settings.permissions.tool.edit.title": "Modifier", - "settings.permissions.tool.edit.description": - "Modifier des fichiers, y compris les modifications, écritures, patchs et multi-modifications", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "Correspondre aux fichiers utilisant des modèles glob", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": - "Rechercher dans le contenu des fichiers à l'aide d'expressions régulières", - "settings.permissions.tool.list.title": "Lister", - "settings.permissions.tool.list.description": "Lister les fichiers dans un répertoire", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "Exécuter des commandes shell", - "settings.permissions.tool.task.title": "Tâche", - "settings.permissions.tool.task.description": "Lancer des sous-agents", - "settings.permissions.tool.skill.title": "Compétence", - "settings.permissions.tool.skill.description": "Charger une compétence par son nom", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "Exécuter des requêtes de serveur de langage", - "settings.permissions.tool.todowrite.title": "Écrire Todo", - "settings.permissions.tool.todowrite.description": "Mettre à jour la liste de tâches", - "settings.permissions.tool.webfetch.title": "Récupération Web", - "settings.permissions.tool.webfetch.description": "Récupérer le contenu d'une URL", - "settings.permissions.tool.websearch.title": "Recherche Web", - "settings.permissions.tool.websearch.description": "Rechercher sur le web", - "settings.permissions.tool.codesearch.title": "Recherche de code", - "settings.permissions.tool.codesearch.description": "Rechercher du code sur le web", - "settings.permissions.tool.external_directory.title": "Répertoire externe", - "settings.permissions.tool.external_directory.description": "Accéder aux fichiers en dehors du répertoire du projet", - "settings.permissions.tool.doom_loop.title": "Boucle infernale", - "settings.permissions.tool.doom_loop.description": "Détecter les appels d'outils répétés avec une entrée identique", - "session.delete.failed.title": "Échec de la suppression de la session", - "session.delete.title": "Supprimer la session", - "session.delete.confirm": 'Supprimer la session "{{name}}" ?', - "session.delete.button": "Supprimer la session", - "workspace.new": "Nouvel espace de travail", - "workspace.type.local": "local", - "workspace.type.sandbox": "bac à sable", - "workspace.create.failed.title": "Échec de la création de l'espace de travail", - "workspace.delete.failed.title": "Échec de la suppression de l'espace de travail", - "workspace.resetting.title": "Réinitialisation de l'espace de travail", - "workspace.resetting.description": "Cela peut prendre une minute.", - "workspace.reset.failed.title": "Échec de la réinitialisation de l'espace de travail", - "workspace.reset.success.title": "Espace de travail réinitialisé", - "workspace.reset.success.description": "L'espace de travail correspond maintenant à la branche par défaut.", - "workspace.error.stillPreparing": "L'espace de travail est encore en cours de préparation", - "workspace.status.checking": "Vérification des modifications non fusionnées...", - "workspace.status.error": "Impossible de vérifier le statut git.", - "workspace.status.clean": "Aucune modification non fusionnée détectée.", - "workspace.status.dirty": "Modifications non fusionnées détectées dans cet espace de travail.", - "workspace.delete.title": "Supprimer l'espace de travail", - "workspace.delete.confirm": 'Supprimer l\'espace de travail "{{name}}" ?', - "workspace.delete.button": "Supprimer l'espace de travail", - "workspace.reset.title": "Réinitialiser l'espace de travail", - "workspace.reset.confirm": 'Réinitialiser l\'espace de travail "{{name}}" ?', - "workspace.reset.button": "Réinitialiser l'espace de travail", - "workspace.reset.archived.none": "Aucune session active ne sera archivée.", - "workspace.reset.archived.one": "1 session sera archivée.", - "workspace.reset.archived.many": "{{count}} sessions seront archivées.", - "workspace.reset.note": "Cela réinitialisera l'espace de travail pour correspondre à la branche par défaut.", - "common.open": "Ouvrir", - "dialog.releaseNotes.action.getStarted": "Commencer", - "dialog.releaseNotes.action.next": "Suivant", - "dialog.releaseNotes.action.hideFuture": "Ne plus afficher à l'avenir", - "dialog.releaseNotes.media.alt": "Aperçu de la version", - "toast.project.reloadFailed.title": "Échec du rechargement de {{project}}", - "error.server.invalidConfiguration": "Configuration invalide", - "common.moreCountSuffix": " (+{{count}} de plus)", - "common.time.justNow": "À l'instant", - "common.time.minutesAgo.short": "il y a {{count}}m", - "common.time.hoursAgo.short": "il y a {{count}}h", - "common.time.daysAgo.short": "il y a {{count}}j", - "settings.providers.connected.environmentDescription": "Connecté à partir de vos variables d'environnement", - "settings.providers.custom.description": "Ajouter un fournisseur compatible avec OpenAI via l'URL de base.", - - "app.server.unreachable": "Impossible de joindre {{server}}", - "app.server.retrying": "Nouvelle tentative automatique...", - "app.server.otherServers": "Autres serveurs", - "dialog.server.add.usernamePlaceholder": "nom d'utilisateur", - "dialog.server.add.passwordPlaceholder": "mot de passe", - "server.row.noUsername": "aucun nom d'utilisateur", - "session.review.noVcs.createGit.title": "Créer un dépôt Git", - "session.review.noVcs.createGit.description": "Suivre, examiner et annuler les modifications dans ce projet", - "session.review.noVcs.createGit.actionLoading": "Création du dépôt Git...", - "session.review.noVcs.createGit.action": "Créer un dépôt Git", - "session.todo.progress": "{{done}} tâches sur {{total}} terminées", - "session.question.progress": "{{current}} questions sur {{total}}", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "Explorateur de fichiers", - "session.header.open.fileManager": "Gestionnaire de fichiers", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "Terminal", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "Diagnostics de performance de développement", - "debugBar.na": "n/a", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": - "Dernière transition de route terminée touchant une page de session, mesurée du début du routeur jusqu'au premier affichage après stabilisation.", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "Images par seconde glissantes sur les 5 dernières secondes.", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "Pire temps d'image sur les 5 dernières secondes.", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "Images de plus de 32ms au cours des 5 dernières secondes.", - "debugBar.long.label": "LONG", - "debugBar.long.tip": - "Temps bloqué et nombre de tâches longues au cours des 5 dernières secondes. Tâche max : {{max}}.", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "Pire délai d'entrée observé au cours des 5 dernières secondes.", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": - "Durée approximative d'interaction au cours des 5 dernières secondes. Ceci est similaire à INP, pas le INP officiel des Web Vitals.", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "Décalage cumulatif de la mise en page pour la durée de vie actuelle de l'application.", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "Tas JS utilisé vs limite de tas. Chromium uniquement.", - "debugBar.mem.tip": "Tas JS utilisé vs limite de tas. {{used}} sur {{limit}}.", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Maj", - "common.key.meta": "Méta", - "common.key.space": "Espace", - "common.key.backspace": "Retour arrière", - "common.key.enter": "Entrée", - "common.key.tab": "Tab", - "common.key.delete": "Suppr", - "common.key.home": "Début", - "common.key.end": "Fin", - "common.key.pageUp": "Page précédente", - "common.key.pageDown": "Page suivante", - "common.key.insert": "Inser", - "common.unknown": "inconnu", - "error.page.circular": "[Circulaire]", - "error.globalSDK.noServerAvailable": "Aucun serveur disponible", - "error.globalSDK.serverNotAvailable": "Serveur non disponible", - "error.childStore.persistedCacheCreateFailed": "Échec de la création du cache persistant", - "error.childStore.persistedProjectMetadataCreateFailed": - "Échec de la création des métadonnées de projet persistantes", - "error.childStore.persistedProjectIconCreateFailed": "Échec de la création de l'icône de projet persistante", - "error.childStore.storeCreateFailed": "Échec de la création du stockage", - "terminal.connectionLost.abnormalClose": "WebSocket fermé anormalement : {{code}}", -} diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts deleted file mode 100644 index 49b8d652d7e..00000000000 --- a/packages/app/src/i18n/ja.ts +++ /dev/null @@ -1,853 +0,0 @@ -export const dict = { - "command.category.suggested": "おすすめ", - "command.category.view": "表示", - "command.category.project": "プロジェクト", - "command.category.provider": "プロバイダー", - "command.category.server": "サーバー", - "command.category.session": "セッション", - "command.category.theme": "テーマ", - "command.category.language": "言語", - "command.category.file": "ファイル", - "command.category.context": "コンテキスト", - "command.category.terminal": "ターミナル", - "command.category.model": "モデル", - "command.category.mcp": "MCP", - "command.category.agent": "エージェント", - "command.category.permissions": "権限", - "command.category.workspace": "ワークスペース", - "command.category.settings": "設定", - "theme.scheme.system": "システム", - "theme.scheme.light": "ライト", - "theme.scheme.dark": "ダーク", - "command.sidebar.toggle": "サイドバーの切り替え", - "command.project.open": "プロジェクトを開く", - "command.provider.connect": "プロバイダーに接続", - "command.server.switch": "サーバーの切り替え", - "command.settings.open": "設定を開く", - "command.session.previous": "前のセッション", - "command.session.next": "次のセッション", - "command.session.previous.unseen": "前の未読セッション", - "command.session.next.unseen": "次の未読セッション", - "command.session.archive": "セッションをアーカイブ", - "command.palette": "コマンドパレット", - "command.theme.cycle": "テーマの切り替え", - "command.theme.set": "テーマを使用: {{theme}}", - "command.theme.scheme.cycle": "配色の切り替え", - "command.theme.scheme.set": "配色を使用: {{scheme}}", - "command.language.cycle": "言語の切り替え", - "command.language.set": "言語を使用: {{language}}", - "command.session.new": "新しいセッション", - "command.file.open": "ファイルを開く", - "command.tab.close": "タブを閉じる", - "command.context.addSelection": "選択範囲をコンテキストに追加", - "command.context.addSelection.description": "現在のファイルから選択した行を追加", - "command.input.focus": "入力欄にフォーカス", - "command.terminal.toggle": "ターミナルの切り替え", - "command.fileTree.toggle": "ファイルツリーを切り替え", - "command.review.toggle": "レビューの切り替え", - "command.terminal.new": "新しいターミナル", - "command.terminal.new.description": "新しいターミナルタブを作成", - "command.steps.toggle": "ステップの切り替え", - "command.steps.toggle.description": "現在のメッセージのステップを表示または非表示", - "command.message.previous": "前のメッセージ", - "command.message.previous.description": "前のユーザーメッセージに移動", - "command.message.next": "次のメッセージ", - "command.message.next.description": "次のユーザーメッセージに移動", - "command.model.choose": "モデルを選択", - "command.model.choose.description": "別のモデルを選択", - "command.mcp.toggle": "MCPの切り替え", - "command.mcp.toggle.description": "MCPを切り替える", - "command.agent.cycle": "エージェントの切り替え", - "command.agent.cycle.description": "次のエージェントに切り替え", - "command.agent.cycle.reverse": "エージェントを逆順に切り替え", - "command.agent.cycle.reverse.description": "前のエージェントに切り替え", - "command.model.variant.cycle": "思考レベルの切り替え", - "command.model.variant.cycle.description": "次の思考レベルに切り替え", - "command.prompt.mode.shell": "シェル", - "command.prompt.mode.normal": "プロンプト", - "command.permissions.autoaccept.enable": "権限を自動承認する", - "command.permissions.autoaccept.disable": "権限の自動承認を停止する", - "command.workspace.toggle": "ワークスペースを切り替え", - "command.workspace.toggle.description": "サイドバーでの複数のワークスペースの有効化・無効化", - "command.session.undo": "元に戻す", - "command.session.undo.description": "最後のメッセージを元に戻す", - "command.session.redo": "やり直す", - "command.session.redo.description": "元に戻したメッセージをやり直す", - "command.session.compact": "セッションを圧縮", - "command.session.compact.description": "セッションを要約してコンテキストサイズを削減", - "command.session.fork": "メッセージからフォーク", - "command.session.fork.description": "以前のメッセージから新しいセッションを作成", - "command.session.share": "セッションを共有", - "command.session.share.description": "このセッションを共有しURLをクリップボードにコピー", - "command.session.unshare": "セッションの共有を停止", - "command.session.unshare.description": "このセッションの共有を停止", - "palette.search.placeholder": "ファイル、コマンド、セッションを検索", - "palette.empty": "結果が見つかりません", - "palette.group.commands": "コマンド", - "palette.group.files": "ファイル", - "dialog.provider.search.placeholder": "プロバイダーを検索", - "dialog.provider.empty": "プロバイダーが見つかりません", - "dialog.provider.group.popular": "人気", - "dialog.provider.group.other": "その他", - "dialog.provider.tag.recommended": "推奨", - "dialog.provider.opencode.note": "Claude, GPT, Geminiなどを含む厳選されたモデル", - "dialog.provider.opencode.tagline": "信頼性の高い最適化モデル", - "dialog.provider.opencodeGo.tagline": "すべての人に低価格のサブスクリプション", - "dialog.provider.anthropic.note": "Claude Pro/MaxまたはAPIキーで接続", - "dialog.provider.copilot.note": "CopilotまたはAPIキーで接続", - "dialog.provider.openai.note": "ChatGPT Pro/PlusまたはAPIキーで接続", - "dialog.provider.google.note": "高速で構造化された応答のためのGeminiモデル", - "dialog.provider.openrouter.note": "1つのプロバイダーからすべてのサポートされているモデルにアクセス", - "dialog.provider.vercel.note": "スマートルーターによるAIモデルへの統合アクセス", - "dialog.model.select.title": "モデルを選択", - "dialog.model.search.placeholder": "モデルを検索", - "dialog.model.empty": "モデルが見つかりません", - "dialog.model.manage": "モデルを管理", - "dialog.model.manage.description": "モデルセレクターに表示するモデルをカスタマイズします。", - "dialog.model.manage.provider.toggle": "すべての{{provider}}モデルを切り替え", - "dialog.model.unpaid.freeModels.title": "Kiloが提供する無料モデル", - "dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加", - "dialog.provider.viewAll": "さらにプロバイダーを表示", - "provider.connect.title": "{{provider}}を接続", - "provider.connect.title.anthropicProMax": "Claude Pro/Maxでログイン", - "provider.connect.selectMethod": "{{provider}}のログイン方法を選択してください。", - "provider.connect.method.apiKey": "APIキー", - "provider.connect.status.inProgress": "認証中...", - "provider.connect.status.waiting": "認証を待機中...", - "provider.connect.status.failed": "認証に失敗しました: {{error}}", - "provider.connect.apiKey.description": - "{{provider}}のAPIキーを入力してアカウントを接続し、Kiloで{{provider}}モデルを使用します。", - "provider.connect.apiKey.label": "{{provider}} APIキー", - "provider.connect.apiKey.placeholder": "APIキー", - "provider.connect.apiKey.required": "APIキーが必要です", - "provider.connect.opencodeZen.line1": - "OpenCode Zenは、コーディングエージェント向けに最適化された信頼性の高いモデルへのアクセスを提供します。", - "provider.connect.opencodeZen.line2": "1つのAPIキーで、Claude、GPT、Gemini、GLMなどのモデルにアクセスできます。", - "provider.connect.opencodeZen.visit.prefix": " ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " にアクセスしてAPIキーを取得してください。", - "provider.connect.oauth.code.visit.prefix": " ", - "provider.connect.oauth.code.visit.link": "このリンク", - "provider.connect.oauth.code.visit.suffix": - " にアクセスして認証コードを取得し、アカウントを接続してKiloで{{provider}}モデルを使用してください。", - "provider.connect.oauth.code.label": "{{method}} 認証コード", - "provider.connect.oauth.code.placeholder": "認証コード", - "provider.connect.oauth.code.required": "認証コードが必要です", - "provider.connect.oauth.code.invalid": "無効な認証コード", - "provider.connect.oauth.auto.visit.prefix": " ", - "provider.connect.oauth.auto.visit.link": "このリンク", - "provider.connect.oauth.auto.visit.suffix": - " にアクセスし、以下のコードを入力してアカウントを接続し、Kiloで{{provider}}モデルを使用してください。", - "provider.connect.oauth.auto.confirmationCode": "確認コード", - "provider.connect.toast.connected.title": "{{provider}}が接続されました", - "provider.connect.toast.connected.description": "{{provider}}モデルが使用可能になりました。", - "provider.custom.title": "カスタムプロバイダー", - "provider.custom.description.prefix": "OpenAI互換のプロバイダーを設定します。詳細は", - "provider.custom.description.link": "プロバイダー設定ドキュメント", - "provider.custom.description.suffix": "をご覧ください。", - "provider.custom.field.providerID.label": "プロバイダーID", - "provider.custom.field.providerID.placeholder": "myprovider", - "provider.custom.field.providerID.description": "小文字、数字、ハイフン、アンダースコア", - "provider.custom.field.name.label": "表示名", - "provider.custom.field.name.placeholder": "My AI Provider", - "provider.custom.field.baseURL.label": "ベースURL", - "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", - "provider.custom.field.apiKey.label": "APIキー", - "provider.custom.field.apiKey.placeholder": "APIキー", - "provider.custom.field.apiKey.description": "オプション。ヘッダーで認証を管理する場合は空のままにしてください。", - "provider.custom.models.label": "モデル", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "model-id", - "provider.custom.models.name.label": "名前", - "provider.custom.models.name.placeholder": "表示名", - "provider.custom.models.remove": "モデルを削除", - "provider.custom.models.add": "モデルを追加", - "provider.custom.headers.label": "ヘッダー (オプション)", - "provider.custom.headers.key.label": "ヘッダー", - "provider.custom.headers.key.placeholder": "Header-Name", - "provider.custom.headers.value.label": "値", - "provider.custom.headers.value.placeholder": "value", - "provider.custom.headers.remove": "ヘッダーを削除", - "provider.custom.headers.add": "ヘッダーを追加", - "provider.custom.error.providerID.required": "プロバイダーIDが必要です", - "provider.custom.error.providerID.format": "小文字、数字、ハイフン、アンダースコアを使用してください", - "provider.custom.error.providerID.exists": "そのプロバイダーIDは既に存在します", - "provider.custom.error.name.required": "表示名が必要です", - "provider.custom.error.baseURL.required": "ベースURLが必要です", - "provider.custom.error.baseURL.format": "http:// または https:// で始まる必要があります", - "provider.custom.error.required": "必須", - "provider.custom.error.duplicate": "重複", - "provider.disconnect.toast.disconnected.title": "{{provider}}が切断されました", - "provider.disconnect.toast.disconnected.description": "{{provider}}のモデルは利用できなくなりました。", - "model.tag.free": "無料", - "model.tag.latest": "最新", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "テキスト", - "model.input.image": "画像", - "model.input.audio": "音声", - "model.input.video": "動画", - "model.input.pdf": "pdf", - "model.tooltip.allows": "対応: {{inputs}}", - "model.tooltip.reasoning.allowed": "推論を許可", - "model.tooltip.reasoning.none": "推論なし", - "model.tooltip.context": "コンテキスト上限 {{limit}}", - "common.search.placeholder": "検索", - "common.goBack": "戻る", - "common.goForward": "進む", - "common.loading": "読み込み中", - "common.loading.ellipsis": "...", - "common.cancel": "キャンセル", - "common.connect": "接続", - "common.disconnect": "切断", - "common.continue": "送信", - "common.submit": "送信", - "common.save": "保存", - "common.saving": "保存中...", - "common.default": "デフォルト", - "common.attachment": "添付ファイル", - "prompt.placeholder.shell": "シェルコマンドを入力... {{example}}", - "prompt.placeholder.normal": '何でも聞いてください... "{{example}}"', - "prompt.placeholder.simple": "何でも聞いてください...", - "prompt.placeholder.summarizeComments": "コメントを要約…", - "prompt.placeholder.summarizeComment": "コメントを要約…", - "prompt.mode.shell": "シェル", - "prompt.mode.normal": "プロンプト", - "prompt.mode.shell.exit": "escで終了", - "prompt.example.1": "コードベースのTODOを修正", - "prompt.example.2": "このプロジェクトの技術スタックは何ですか?", - "prompt.example.3": "壊れたテストを修正", - "prompt.example.4": "認証の仕組みを説明して", - "prompt.example.5": "セキュリティの脆弱性を見つけて修正", - "prompt.example.6": "ユーザーサービスのユニットテストを追加", - "prompt.example.7": "この関数を読みやすくリファクタリング", - "prompt.example.8": "このエラーはどういう意味ですか?", - "prompt.example.9": "この問題のデバッグを手伝って", - "prompt.example.10": "APIドキュメントを生成", - "prompt.example.11": "データベースクエリを最適化", - "prompt.example.12": "入力バリデーションを追加", - "prompt.example.13": "〜の新しいコンポーネントを作成", - "prompt.example.14": "このプロジェクトをデプロイするには?", - "prompt.example.15": "ベストプラクティスの観点でコードをレビュー", - "prompt.example.16": "この関数にエラーハンドリングを追加", - "prompt.example.17": "この正規表現パターンを説明して", - "prompt.example.18": "これをTypeScriptに変換", - "prompt.example.19": "コードベース全体にログを追加", - "prompt.example.20": "古い依存関係はどれですか?", - "prompt.example.21": "マイグレーションスクリプトの作成を手伝って", - "prompt.example.22": "このエンドポイントにキャッシュを実装", - "prompt.example.23": "このリストにページネーションを追加", - "prompt.example.24": "〜のCLIコマンドを作成", - "prompt.example.25": "ここでは環境変数はどう機能しますか?", - "prompt.popover.emptyResults": "一致する結果がありません", - "prompt.popover.emptyCommands": "一致するコマンドがありません", - "prompt.dropzone.label": "画像、PDF、またはテキストファイルをここにドロップしてください", - "prompt.dropzone.file.label": "ドロップして@メンションファイルを追加", - "prompt.slash.badge.custom": "カスタム", - "prompt.slash.badge.skill": "スキル", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "アクティブ", - "prompt.context.includeActiveFile": "アクティブなファイルを含める", - "prompt.context.removeActiveFile": "コンテキストからアクティブなファイルを削除", - "prompt.context.removeFile": "コンテキストからファイルを削除", - "prompt.action.attachFile": "ファイルを添付", - "prompt.attachment.remove": "添付ファイルを削除", - "prompt.action.send": "送信", - "prompt.action.stop": "停止", - "prompt.toast.pasteUnsupported.title": "サポートされていない添付ファイル", - "prompt.toast.pasteUnsupported.description": "画像、PDF、またはテキストファイルのみ添付できます。", - "prompt.toast.modelAgentRequired.title": "エージェントとモデルを選択", - "prompt.toast.modelAgentRequired.description": "プロンプトを送信する前にエージェントとモデルを選択してください。", - "prompt.toast.worktreeCreateFailed.title": "ワークツリーの作成に失敗しました", - "prompt.toast.sessionCreateFailed.title": "セッションの作成に失敗しました", - "prompt.toast.shellSendFailed.title": "シェルコマンドの送信に失敗しました", - "prompt.toast.commandSendFailed.title": "コマンドの送信に失敗しました", - "prompt.toast.promptSendFailed.title": "プロンプトの送信に失敗しました", - "prompt.toast.promptSendFailed.description": "セッションを取得できませんでした", - "dialog.mcp.title": "MCP", - "dialog.mcp.description": "{{total}}個中{{enabled}}個が有効", - "dialog.mcp.empty": "MCPが設定されていません", - "dialog.lsp.empty": "ファイルタイプから自動検出されたLSP", - "dialog.plugins.empty": "opencode.jsonで設定されたプラグイン", - "mcp.status.connected": "接続済み", - "mcp.status.failed": "失敗", - "mcp.status.needs_auth": "認証が必要", - "mcp.status.disabled": "無効", - "dialog.fork.empty": "フォーク元のメッセージがありません", - "dialog.directory.search.placeholder": "フォルダを検索", - "dialog.directory.empty": "フォルダが見つかりません", - "dialog.server.title": "サーバー", - "dialog.server.description": "このアプリが接続するKiloサーバーを切り替えます。", - "dialog.server.search.placeholder": "サーバーを検索", - "dialog.server.empty": "サーバーはまだありません", - "dialog.server.add.title": "サーバーを追加", - "dialog.server.add.url": "サーバーURL", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "サーバーに接続できませんでした", - "dialog.server.add.checking": "確認中...", - "dialog.server.add.button": "サーバーを追加", - "dialog.server.add.name": "サーバー名 (オプション)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "ユーザー名 (オプション)", - "dialog.server.add.password": "パスワード (オプション)", - "dialog.server.edit.title": "サーバーを編集", - "dialog.server.default.title": "デフォルトサーバー", - "dialog.server.default.description": - "ローカルサーバーを起動する代わりに、アプリ起動時にこのサーバーに接続します。再起動が必要です。", - "dialog.server.default.none": "サーバーが選択されていません", - "dialog.server.default.set": "現在のサーバーをデフォルトに設定", - "dialog.server.default.clear": "クリア", - "dialog.server.action.remove": "サーバーを削除", - "dialog.server.menu.edit": "編集", - "dialog.server.menu.default": "デフォルトに設定", - "dialog.server.menu.defaultRemove": "デフォルト設定を解除", - "dialog.server.menu.delete": "削除", - "dialog.server.current": "現在のサーバー", - "dialog.server.status.default": "デフォルト", - "dialog.project.edit.title": "プロジェクトを編集", - "dialog.project.edit.name": "名前", - "dialog.project.edit.icon": "アイコン", - "dialog.project.edit.icon.alt": "プロジェクトアイコン", - "dialog.project.edit.icon.hint": "クリックまたは画像をドラッグ", - "dialog.project.edit.icon.recommended": "推奨: 128x128px", - "dialog.project.edit.color": "色", - "dialog.project.edit.color.select": "{{color}}の色を選択", - "dialog.project.edit.worktree.startup": "ワークスペース起動スクリプト", - "dialog.project.edit.worktree.startup.description": - "新しいワークスペース (ワークツリー) を作成した後に実行されます。", - "dialog.project.edit.worktree.startup.placeholder": "例: bun install", - "context.breakdown.title": "コンテキストの内訳", - "context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。', - "context.breakdown.system": "システム", - "context.breakdown.user": "ユーザー", - "context.breakdown.assistant": "アシスタント", - "context.breakdown.tool": "ツール呼び出し", - "context.breakdown.other": "その他", - "context.systemPrompt.title": "システムプロンプト", - "context.rawMessages.title": "生のメッセージ", - "context.stats.session": "セッション", - "context.stats.messages": "メッセージ", - "context.stats.provider": "プロバイダー", - "context.stats.model": "モデル", - "context.stats.limit": "コンテキスト制限", - "context.stats.totalTokens": "総トークン数", - "context.stats.usage": "使用量", - "context.stats.inputTokens": "入力トークン", - "context.stats.outputTokens": "出力トークン", - "context.stats.reasoningTokens": "推論トークン", - "context.stats.cacheTokens": "キャッシュトークン (読込/書込)", - "context.stats.userMessages": "ユーザーメッセージ", - "context.stats.assistantMessages": "アシスタントメッセージ", - "context.stats.totalCost": "総コスト", - "context.stats.sessionCreated": "セッション作成日時", - "context.stats.lastActivity": "最終アクティビティ", - "context.usage.tokens": "トークン", - "context.usage.usage": "使用量", - "context.usage.cost": "コスト", - "context.usage.clickToView": "クリックしてコンテキストを表示", - "context.usage.view": "コンテキスト使用量を表示", - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - "toast.language.title": "言語", - "toast.language.description": "{{language}}に切り替えました", - "toast.theme.title": "テーマが切り替わりました", - "toast.scheme.title": "配色", - "toast.workspace.enabled.title": "ワークスペースが有効になりました", - "toast.workspace.enabled.description": "サイドバーに複数のワークツリーが表示されます", - "toast.workspace.disabled.title": "ワークスペースが無効になりました", - "toast.workspace.disabled.description": "サイドバーにはメインのワークツリーのみが表示されます", - "toast.permissions.autoaccept.on.title": "権限を自動承認しています", - "toast.permissions.autoaccept.on.description": "権限の要求は自動的に承認されます", - "toast.permissions.autoaccept.off.title": "権限の自動承認を停止しました", - "toast.permissions.autoaccept.off.description": "権限の要求には承認が必要になります", - "toast.model.none.title": "モデルが選択されていません", - "toast.model.none.description": "このセッションを要約するにはプロバイダーを接続してください", - "toast.file.loadFailed.title": "ファイルの読み込みに失敗しました", - "toast.file.listFailed.title": "ファイル一覧の取得に失敗しました", - "toast.context.noLineSelection.title": "行が選択されていません", - "toast.context.noLineSelection.description": "まずファイルタブで行範囲を選択してください。", - "toast.session.share.copyFailed.title": "URLのコピーに失敗しました", - "toast.session.share.success.title": "セッションを共有しました", - "toast.session.share.success.description": "共有URLをクリップボードにコピーしました!", - "toast.session.share.failed.title": "セッションの共有に失敗しました", - "toast.session.share.failed.description": "セッションの共有中にエラーが発生しました", - "toast.session.unshare.success.title": "セッションの共有を解除しました", - "toast.session.unshare.success.description": "セッションの共有解除に成功しました!", - "toast.session.unshare.failed.title": "セッションの共有解除に失敗しました", - "toast.session.unshare.failed.description": "セッションの共有解除中にエラーが発生しました", - "toast.session.listFailed.title": "{{project}}のセッション読み込みに失敗しました", - "toast.update.title": "アップデートが利用可能です", - "toast.update.description": "Kiloの新しいバージョン ({{version}}) がインストール可能です。", - "toast.update.action.installRestart": "インストールして再起動", - "toast.update.action.notYet": "今はしない", - "error.page.title": "問題が発生しました", - "error.page.description": "アプリケーションの読み込み中にエラーが発生しました。", - "error.page.details.label": "エラー詳細", - "error.page.action.restart": "再起動", - "error.page.action.checking": "確認中...", - "error.page.action.checkUpdates": "アップデートを確認", - "error.page.action.updateTo": "{{version}}にアップデート", - "error.page.report.prefix": "このエラーをKiloチームに報告してください: ", - "error.page.report.discord": "Discord", - "error.page.version": "バージョン: {{version}}", - "error.dev.rootNotFound": - "ルート要素が見つかりません。index.htmlに追加するのを忘れていませんか?またはid属性のスペルが間違っていませんか?", - "error.globalSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?", - "directory.error.invalidUrl": "URL内のディレクトリが無効です。", - "error.chain.unknown": "不明なエラー", - "error.chain.causedBy": "原因:", - "error.chain.apiError": "APIエラー", - "error.chain.status": "ステータス: {{status}}", - "error.chain.retryable": "再試行可能: {{retryable}}", - "error.chain.responseBody": "レスポンス本文:\n{{body}}", - "error.chain.didYouMean": "もしかして: {{suggestions}}", - "error.chain.modelNotFound": "モデルが見つかりません: {{provider}}/{{model}}", - "error.chain.checkConfig": "config (opencode.json) のプロバイダー/モデル名を確認してください", - "error.chain.mcpFailed": 'MCPサーバー "{{name}}" が失敗しました。注意: KiloはまだMCP認証をサポートしていません。', - "error.chain.providerAuthFailed": "プロバイダー認証に失敗しました ({{provider}}): {{message}}", - "error.chain.providerInitFailed": - 'プロバイダー "{{provider}}" の初期化に失敗しました。認証情報と設定を確認してください。', - "error.chain.configJsonInvalid": "{{path}} の設定ファイルは有効なJSON(C)ではありません", - "error.chain.configJsonInvalidWithMessage": "{{path}} の設定ファイルは有効なJSON(C)ではありません: {{message}}", - "error.chain.configDirectoryTypo": - '{{path}} 内のディレクトリ "{{dir}}" は無効です。"{{suggestion}}" に名前を変更するか削除してください。これはよくあるタイプミスです。', - "error.chain.configFrontmatterError": "{{path}} のフロントマターの解析に失敗しました:\n{{message}}", - "error.chain.configInvalid": "{{path}} の設定ファイルが無効です", - "error.chain.configInvalidWithMessage": "{{path}} の設定ファイルが無効です: {{message}}", - "notification.permission.title": "権限が必要です", - "notification.permission.description": "{{projectName}} の {{sessionTitle}} が権限を必要としています", - "notification.question.title": "質問", - "notification.question.description": "{{projectName}} の {{sessionTitle}} から質問があります", - "notification.action.goToSession": "セッションへ移動", - "notification.session.responseReady.title": "応答の準備ができました", - "notification.session.error.title": "セッションエラー", - "notification.session.error.fallbackDescription": "エラーが発生しました", - "home.recentProjects": "最近のプロジェクト", - "home.empty.title": "最近のプロジェクトはありません", - "home.empty.description": "ローカルプロジェクトを開いて始めましょう", - "session.tab.session": "セッション", - "session.tab.review": "レビュー", - "session.tab.context": "コンテキスト", - "session.panel.reviewAndFiles": "レビューとファイル", - "session.review.filesChanged": "{{count}} ファイル変更", - "session.review.change.one": "変更", - "session.review.change.other": "変更", - "session.review.loadingChanges": "変更を読み込み中...", - "session.review.empty": "このセッションでの変更はまだありません", - "session.review.noVcs": "Gitバージョン管理システムが検出されないため、変更は表示されません", - "session.review.noSnapshot": "設定でスナップショット追跡が無効になっているため、セッションの変更は利用できません", - "session.review.noChanges": "変更なし", - "session.files.selectToOpen": "開くファイルを選択", - "session.files.all": "すべてのファイル", - "session.files.empty": "ファイルなし", - "session.files.binaryContent": "バイナリファイル(内容を表示できません)", - "session.messages.renderEarlier": "以前のメッセージを表示", - "session.messages.loadingEarlier": "以前のメッセージを読み込み中...", - "session.messages.loadEarlier": "以前のメッセージを読み込む", - "session.messages.loading": "メッセージを読み込み中...", - "session.messages.jumpToLatest": "最新へジャンプ", - "session.context.addToContext": "{{selection}}をコンテキストに追加", - "session.todo.title": "ToDo", - "session.todo.collapse": "折りたたむ", - "session.todo.expand": "展開", - "session.followupDock.summary.one": "{{count}} 件のメッセージが待機中", - "session.followupDock.summary.other": "{{count}} 件のメッセージが待機中", - "session.followupDock.sendNow": "今すぐ送信", - "session.followupDock.edit": "編集", - "session.followupDock.collapse": "待機中のメッセージを折りたたむ", - "session.followupDock.expand": "待機中のメッセージを展開", - "session.revertDock.summary.one": "{{count}} 件のロールバックされたメッセージ", - "session.revertDock.summary.other": "{{count}} 件のロールバックされたメッセージ", - "session.revertDock.collapse": "ロールバックされたメッセージを折りたたむ", - "session.revertDock.expand": "ロールバックされたメッセージを展開", - "session.revertDock.restore": "メッセージを復元", - "session.new.title": "何でも作る", - "session.new.worktree.main": "メインブランチ", - "session.new.worktree.mainWithBranch": "メインブランチ ({{branch}})", - "session.new.worktree.create": "新しいワークツリーを作成", - "session.new.lastModified": "最終更新", - "session.header.search.placeholder": "{{project}}を検索", - "session.header.searchFiles": "ファイルを検索", - "session.header.openIn": "で開く", - "session.header.open.action": "{{app}}を開く", - "session.header.open.ariaLabel": "{{app}}で開く", - "session.header.open.menu": "開くオプション", - "session.header.open.copyPath": "パスをコピー", - "status.popover.trigger": "ステータス", - "status.popover.ariaLabel": "サーバー設定", - "status.popover.tab.servers": "サーバー", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "プラグイン", - "status.popover.action.manageServers": "サーバーを管理", - "session.share.popover.title": "ウェブで公開", - "session.share.popover.description.shared": - "このセッションはウェブで公開されています。リンクを知っている人なら誰でもアクセスできます。", - "session.share.popover.description.unshared": - "セッションをウェブで公開します。リンクを知っている人なら誰でもアクセスできるようになります。", - "session.share.action.share": "共有", - "session.share.action.publish": "公開", - "session.share.action.publishing": "公開中...", - "session.share.action.unpublish": "非公開にする", - "session.share.action.unpublishing": "非公開にしています...", - "session.share.action.view": "表示", - "session.share.copy.copied": "コピーしました", - "session.share.copy.copyLink": "リンクをコピー", - "lsp.tooltip.none": "LSPサーバーなし", - "lsp.label.connected": "{{count}} LSP", - "prompt.loading": "プロンプトを読み込み中...", - "terminal.loading": "ターミナルを読み込み中...", - "terminal.title": "ターミナル", - "terminal.title.numbered": "ターミナル {{number}}", - "terminal.close": "ターミナルを閉じる", - "terminal.connectionLost.title": "接続が失われました", - "terminal.connectionLost.description": - "ターミナルの接続が中断されました。これはサーバーが再起動したときに発生することがあります。", - "common.closeTab": "タブを閉じる", - "common.dismiss": "閉じる", - "common.requestFailed": "リクエスト失敗", - "common.moreOptions": "その他のオプション", - "common.learnMore": "詳細", - "common.rename": "名前変更", - "common.reset": "リセット", - "common.archive": "アーカイブ", - "common.delete": "削除", - "common.close": "閉じる", - "common.edit": "編集", - "common.loadMore": "さらに読み込む", - "common.key.esc": "ESC", - "sidebar.menu.toggle": "メニューを切り替え", - "sidebar.nav.projectsAndSessions": "プロジェクトとセッション", - "sidebar.settings": "設定", - "sidebar.help": "ヘルプ", - "sidebar.workspaces.enable": "ワークスペースを有効化", - "sidebar.workspaces.disable": "ワークスペースを無効化", - "sidebar.gettingStarted.title": "はじめに", - "sidebar.gettingStarted.line1": "Kiloには無料モデルが含まれているため、すぐに開始できます。", - "sidebar.gettingStarted.line2": "プロバイダーを接続して、Claude、GPT、Geminiなどのモデルを使用できます。", - "sidebar.project.recentSessions": "最近のセッション", - "sidebar.project.viewAllSessions": "すべてのセッションを表示", - "sidebar.project.clearNotifications": "通知をクリア", - "app.name.desktop": "Kilo Desktop", - "settings.section.desktop": "デスクトップ", - "settings.section.server": "サーバー", - "settings.tab.general": "一般", - "settings.tab.shortcuts": "ショートカット", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL連携", - "settings.desktop.wsl.description": "WindowsのWSL環境でKiloサーバーを実行します。", - "settings.general.section.appearance": "外観", - "settings.general.section.notifications": "システム通知", - "settings.general.section.updates": "アップデート", - "settings.general.section.sounds": "効果音", - "settings.general.section.feed": "フィード", - "settings.general.section.display": "ディスプレイ", - "settings.general.row.language.title": "言語", - "settings.general.row.language.description": "Kiloの表示言語を変更します", - "settings.general.row.appearance.title": "外観", - "settings.general.row.appearance.description": "デバイスでのKiloの表示をカスタマイズします", - "settings.general.row.colorScheme.title": "配色", - "settings.general.row.colorScheme.description": "Kiloがシステム、ライト、またはダークテーマに従うかを選択します", - "settings.general.row.theme.title": "テーマ", - "settings.general.row.theme.description": "Kiloのテーマをカスタマイズします。", - "settings.general.row.font.title": "コードフォント", - "settings.general.row.font.description": "コードブロックで使用するフォントをカスタマイズします", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "UIフォント", - "settings.general.row.uiFont.description": "インターフェース全体で使用するフォントをカスタマイズします", - "settings.general.row.followup.title": "フォローアップの動作", - "settings.general.row.followup.description": - "フォローアッププロンプトを即座に実行するか、キューで待機させるかを選択します", - "settings.general.row.followup.option.queue": "キューに追加", - "settings.general.row.followup.option.steer": "即座に実行 (Steer)", - "settings.general.row.reasoningSummaries.title": "推論の要約を表示", - "settings.general.row.reasoningSummaries.description": "タイムラインにモデルの推論の要約を表示します", - "settings.general.row.shellToolPartsExpanded.title": "shell ツールパーツを展開", - "settings.general.row.shellToolPartsExpanded.description": - "タイムラインで shell ツールパーツをデフォルトで展開して表示します", - "settings.general.row.editToolPartsExpanded.title": "edit ツールパーツを展開", - "settings.general.row.editToolPartsExpanded.description": - "タイムラインで edit、write、patch ツールパーツをデフォルトで展開して表示します", - "settings.general.row.showSessionProgressBar.title": "セッション進行状況バーを表示", - "settings.general.row.showSessionProgressBar.description": - "エージェントの作業中に、セッション上部にアニメーション付きの進行状況バーを表示します", - "settings.general.row.wayland.title": "ネイティブWaylandを使用", - "settings.general.row.wayland.description": "WaylandでのX11フォールバックを無効にします。再起動が必要です。", - "settings.general.row.wayland.tooltip": - "リフレッシュレートが混在するモニターを使用しているLinuxでは、ネイティブWaylandの方が安定する場合があります。", - "settings.general.row.releaseNotes.title": "リリースノート", - "settings.general.row.releaseNotes.description": "アップデート後に「新機能」ポップアップを表示", - "settings.updates.row.startup.title": "起動時にアップデートを確認", - "settings.updates.row.startup.description": "Kilo の起動時に自動でアップデートを確認します", - "settings.updates.row.check.title": "アップデートを確認", - "settings.updates.row.check.description": "手動でアップデートを確認し、利用可能ならインストールします", - "settings.updates.action.checkNow": "今すぐ確認", - "settings.updates.action.checking": "確認中...", - "settings.updates.toast.latest.title": "最新です", - "settings.updates.toast.latest.description": "Kilo は最新バージョンです。", - "sound.option.none": "なし", - "sound.option.alert01": "アラート 01", - "sound.option.alert02": "アラート 02", - "sound.option.alert03": "アラート 03", - "sound.option.alert04": "アラート 04", - "sound.option.alert05": "アラート 05", - "sound.option.alert06": "アラート 06", - "sound.option.alert07": "アラート 07", - "sound.option.alert08": "アラート 08", - "sound.option.alert09": "アラート 09", - "sound.option.alert10": "アラート 10", - "sound.option.bipbop01": "ビップボップ 01", - "sound.option.bipbop02": "ビップボップ 02", - "sound.option.bipbop03": "ビップボップ 03", - "sound.option.bipbop04": "ビップボップ 04", - "sound.option.bipbop05": "ビップボップ 05", - "sound.option.bipbop06": "ビップボップ 06", - "sound.option.bipbop07": "ビップボップ 07", - "sound.option.bipbop08": "ビップボップ 08", - "sound.option.bipbop09": "ビップボップ 09", - "sound.option.bipbop10": "ビップボップ 10", - "sound.option.staplebops01": "ステープルボップス 01", - "sound.option.staplebops02": "ステープルボップス 02", - "sound.option.staplebops03": "ステープルボップス 03", - "sound.option.staplebops04": "ステープルボップス 04", - "sound.option.staplebops05": "ステープルボップス 05", - "sound.option.staplebops06": "ステープルボップス 06", - "sound.option.staplebops07": "ステープルボップス 07", - "sound.option.nope01": "いいえ 01", - "sound.option.nope02": "いいえ 02", - "sound.option.nope03": "いいえ 03", - "sound.option.nope04": "いいえ 04", - "sound.option.nope05": "いいえ 05", - "sound.option.nope06": "いいえ 06", - "sound.option.nope07": "いいえ 07", - "sound.option.nope08": "いいえ 08", - "sound.option.nope09": "いいえ 09", - "sound.option.nope10": "いいえ 10", - "sound.option.nope11": "いいえ 11", - "sound.option.nope12": "いいえ 12", - "sound.option.yup01": "はい 01", - "sound.option.yup02": "はい 02", - "sound.option.yup03": "はい 03", - "sound.option.yup04": "はい 04", - "sound.option.yup05": "はい 05", - "sound.option.yup06": "はい 06", - "settings.general.notifications.agent.title": "エージェント", - "settings.general.notifications.agent.description": - "エージェントが完了したか、注意が必要な場合にシステム通知を表示します", - "settings.general.notifications.permissions.title": "権限", - "settings.general.notifications.permissions.description": "権限が必要な場合にシステム通知を表示します", - "settings.general.notifications.errors.title": "エラー", - "settings.general.notifications.errors.description": "エラーが発生した場合にシステム通知を表示します", - "settings.general.sounds.agent.title": "エージェント", - "settings.general.sounds.agent.description": "エージェントが完了したか、注意が必要な場合に音を再生します", - "settings.general.sounds.permissions.title": "権限", - "settings.general.sounds.permissions.description": "権限が必要な場合に音を再生します", - "settings.general.sounds.errors.title": "エラー", - "settings.general.sounds.errors.description": "エラーが発生した場合に音を再生します", - "settings.shortcuts.title": "キーボードショートカット", - "settings.shortcuts.reset.button": "デフォルトにリセット", - "settings.shortcuts.reset.toast.title": "ショートカットをリセットしました", - "settings.shortcuts.reset.toast.description": "キーボードショートカットがデフォルトにリセットされました。", - "settings.shortcuts.conflict.title": "ショートカットは既に使用されています", - "settings.shortcuts.conflict.description": "{{keybind}} は既に {{titles}} に割り当てられています。", - "settings.shortcuts.unassigned": "未割り当て", - "settings.shortcuts.pressKeys": "キーを押してください", - "settings.shortcuts.search.placeholder": "ショートカットを検索", - "settings.shortcuts.search.empty": "ショートカットが見つかりません", - "settings.shortcuts.group.general": "一般", - "settings.shortcuts.group.session": "セッション", - "settings.shortcuts.group.navigation": "ナビゲーション", - "settings.shortcuts.group.modelAndAgent": "モデルとエージェント", - "settings.shortcuts.group.terminal": "ターミナル", - "settings.shortcuts.group.prompt": "プロンプト", - "settings.providers.title": "プロバイダー", - "settings.providers.description": "プロバイダー設定はここで構成できます。", - "settings.providers.section.connected": "接続済みプロバイダー", - "settings.providers.connected.empty": "接続済みプロバイダーはありません", - "settings.providers.section.popular": "人気のプロバイダー", - "settings.providers.tag.environment": "環境", - "settings.providers.tag.config": "設定", - "settings.providers.tag.custom": "カスタム", - "settings.providers.tag.other": "その他", - "settings.models.title": "モデル", - "settings.models.description": "モデル設定はここで構成できます。", - "settings.agents.title": "エージェント", - "settings.agents.description": "エージェント設定はここで構成できます。", - "settings.commands.title": "コマンド", - "settings.commands.description": "コマンド設定はここで構成できます。", - "settings.mcp.title": "MCP", - "settings.mcp.description": "MCP設定はここで構成できます。", - "settings.permissions.title": "権限", - "settings.permissions.description": "サーバーがデフォルトで使用できるツールを制御します。", - "settings.permissions.section.tools": "ツール", - "settings.permissions.toast.updateFailed.title": "権限の更新に失敗しました", - "settings.permissions.action.allow": "許可", - "settings.permissions.action.ask": "確認", - "settings.permissions.action.deny": "拒否", - "settings.permissions.tool.read.title": "読み込み", - "settings.permissions.tool.read.description": "ファイルの読み込み (ファイルパスに一致)", - "settings.permissions.tool.edit.title": "編集", - "settings.permissions.tool.edit.description": "ファイルの変更(編集、書き込み、パッチ、複数編集を含む)", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "Globパターンを使用したファイルの一致", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "正規表現を使用したファイル内容の検索", - "settings.permissions.tool.list.title": "リスト", - "settings.permissions.tool.list.description": "ディレクトリ内のファイル一覧表示", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "シェルコマンドの実行", - "settings.permissions.tool.task.title": "タスク", - "settings.permissions.tool.task.description": "サブエージェントの起動", - "settings.permissions.tool.skill.title": "スキル", - "settings.permissions.tool.skill.description": "名前によるスキルの読み込み", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "言語サーバークエリの実行", - "settings.permissions.tool.todowrite.title": "Todo書き込み", - "settings.permissions.tool.todowrite.description": "Todoリストの更新", - "settings.permissions.tool.webfetch.title": "Web取得", - "settings.permissions.tool.webfetch.description": "URLからコンテンツを取得", - "settings.permissions.tool.websearch.title": "Web検索", - "settings.permissions.tool.websearch.description": "ウェブを検索", - "settings.permissions.tool.codesearch.title": "コード検索", - "settings.permissions.tool.codesearch.description": "ウェブ上のコードを検索", - "settings.permissions.tool.external_directory.title": "外部ディレクトリ", - "settings.permissions.tool.external_directory.description": "プロジェクトディレクトリ外のファイルへのアクセス", - "settings.permissions.tool.doom_loop.title": "無限ループ", - "settings.permissions.tool.doom_loop.description": "同一入力による繰り返しのツール呼び出しを検出", - "session.delete.failed.title": "セッションの削除に失敗しました", - "session.delete.title": "セッションの削除", - "session.delete.confirm": 'セッション "{{name}}" を削除しますか?', - "session.delete.button": "セッションを削除", - "workspace.new": "新しいワークスペース", - "workspace.type.local": "ローカル", - "workspace.type.sandbox": "サンドボックス", - "workspace.create.failed.title": "ワークスペースの作成に失敗しました", - "workspace.delete.failed.title": "ワークスペースの削除に失敗しました", - "workspace.resetting.title": "ワークスペースをリセット中", - "workspace.resetting.description": "これには少し時間がかかる場合があります。", - "workspace.reset.failed.title": "ワークスペースのリセットに失敗しました", - "workspace.reset.success.title": "ワークスペースをリセットしました", - "workspace.reset.success.description": "ワークスペースはデフォルトブランチと一致しています。", - "workspace.error.stillPreparing": "ワークスペースはまだ準備中です", - "workspace.status.checking": "未マージの変更を確認中...", - "workspace.status.error": "gitステータスを確認できません。", - "workspace.status.clean": "未マージの変更は検出されませんでした。", - "workspace.status.dirty": "このワークスペースで未マージの変更が検出されました。", - "workspace.delete.title": "ワークスペースの削除", - "workspace.delete.confirm": 'ワークスペース "{{name}}" を削除しますか?', - "workspace.delete.button": "ワークスペースを削除", - "workspace.reset.title": "ワークスペースのリセット", - "workspace.reset.confirm": 'ワークスペース "{{name}}" をリセットしますか?', - "workspace.reset.button": "ワークスペースをリセット", - "workspace.reset.archived.none": "アクティブなセッションはアーカイブされません。", - "workspace.reset.archived.one": "1つのセッションがアーカイブされます。", - "workspace.reset.archived.many": "{{count}}個のセッションがアーカイブされます。", - "workspace.reset.note": "これにより、ワークスペースはデフォルトブランチと一致するようにリセットされます。", - "common.open": "開く", - "dialog.releaseNotes.action.getStarted": "始める", - "dialog.releaseNotes.action.next": "次へ", - "dialog.releaseNotes.action.hideFuture": "今後表示しない", - "dialog.releaseNotes.media.alt": "リリースのプレビュー", - "toast.project.reloadFailed.title": "{{project}} の再読み込みに失敗しました", - "error.server.invalidConfiguration": "無効な設定", - "common.moreCountSuffix": " (他 {{count}} 件)", - "common.time.justNow": "たった今", - "common.time.minutesAgo.short": "{{count}} 分前", - "common.time.hoursAgo.short": "{{count}} 時間前", - "common.time.daysAgo.short": "{{count}} 日前", - "settings.providers.connected.environmentDescription": "環境変数から接続されました", - "settings.providers.custom.description": "ベース URL を指定して OpenAI 互換のプロバイダーを追加します。", - - "app.server.unreachable": "{{server}} に到達できませんでした", - "app.server.retrying": "自動的に再試行中...", - "app.server.otherServers": "その他のサーバー", - "dialog.server.add.usernamePlaceholder": "ユーザー名", - "dialog.server.add.passwordPlaceholder": "パスワード", - "server.row.noUsername": "ユーザー名なし", - "session.review.noVcs.createGit.title": "Git リポジトリを作成", - "session.review.noVcs.createGit.description": "このプロジェクトの変更を追跡、レビュー、元に戻す", - "session.review.noVcs.createGit.actionLoading": "Git リポジトリを作成中...", - "session.review.noVcs.createGit.action": "Git リポジトリを作成", - "session.todo.progress": "{{done}} 個中 {{total}} 個の Todo が完了", - "session.question.progress": "{{total}} 問中 {{current}} 問", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "エクスプローラー", - "session.header.open.fileManager": "ファイルマネージャー", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "ターミナル", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "開発パフォーマンス診断", - "debugBar.na": "n/a", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": "セッションページに触れる最後に完了したルート遷移。ルーター開始から安定後の最初の描画まで測定。", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "過去5秒間のローリングフレーム/秒。", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "過去5秒間の最悪フレーム時間。", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "過去5秒間で32msを超えたフレーム。", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "過去5秒間のブロック時間と長時間タスク数。最大タスク: {{max}}。", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "過去5秒間で観測された最悪の入力遅延。", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": - "過去5秒間の概算インタラクション時間。これは INP に似ていますが、公式の Web Vitals INP ではありません。", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "現在のアプリ寿命の累積レイアウトシフト。", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "使用中の JS ヒープ対ヒープ制限。Chromium のみ。", - "debugBar.mem.tip": "使用中の JS ヒープ対ヒープ制限。{{limit}} 中 {{used}}。", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "Space", - "common.key.backspace": "Backspace", - "common.key.enter": "Enter", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "不明", - "error.page.circular": "[循環]", - "error.globalSDK.noServerAvailable": "利用可能なサーバーがありません", - "error.globalSDK.serverNotAvailable": "サーバーが利用できません", - "error.childStore.persistedCacheCreateFailed": "永続キャッシュの作成に失敗しました", - "error.childStore.persistedProjectMetadataCreateFailed": "永続プロジェクトメタデータの作成に失敗しました", - "error.childStore.persistedProjectIconCreateFailed": "永続プロジェクトアイコンの作成に失敗しました", - "error.childStore.storeCreateFailed": "ストアの作成に失敗しました", - "terminal.connectionLost.abnormalClose": "WebSocket が異常終了しました: {{code}}", -} diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts deleted file mode 100644 index c1c6f397ccc..00000000000 --- a/packages/app/src/i18n/ko.ts +++ /dev/null @@ -1,848 +0,0 @@ -export const dict = { - "command.category.suggested": "추천", - "command.category.view": "보기", - "command.category.project": "프로젝트", - "command.category.provider": "공급자", - "command.category.server": "서버", - "command.category.session": "세션", - "command.category.theme": "테마", - "command.category.language": "언어", - "command.category.file": "파일", - "command.category.context": "컨텍스트", - "command.category.terminal": "터미널", - "command.category.model": "모델", - "command.category.mcp": "MCP", - "command.category.agent": "에이전트", - "command.category.permissions": "권한", - "command.category.workspace": "작업 공간", - "command.category.settings": "설정", - "theme.scheme.system": "시스템", - "theme.scheme.light": "라이트", - "theme.scheme.dark": "다크", - "command.sidebar.toggle": "사이드바 토글", - "command.project.open": "프로젝트 열기", - "command.provider.connect": "공급자 연결", - "command.server.switch": "서버 전환", - "command.settings.open": "설정 열기", - "command.session.previous": "이전 세션", - "command.session.next": "다음 세션", - "command.session.previous.unseen": "이전 읽지 않은 세션", - "command.session.next.unseen": "다음 읽지 않은 세션", - "command.session.archive": "세션 보관", - "command.palette": "명령 팔레트", - "command.theme.cycle": "테마 순환", - "command.theme.set": "테마 사용: {{theme}}", - "command.theme.scheme.cycle": "색상 테마 순환", - "command.theme.scheme.set": "색상 테마 사용: {{scheme}}", - "command.language.cycle": "언어 순환", - "command.language.set": "언어 사용: {{language}}", - "command.session.new": "새 세션", - "command.file.open": "파일 열기", - "command.tab.close": "탭 닫기", - "command.context.addSelection": "선택 영역을 컨텍스트에 추가", - "command.context.addSelection.description": "현재 파일에서 선택한 줄을 추가", - "command.input.focus": "입력창 포커스", - "command.terminal.toggle": "터미널 토글", - "command.fileTree.toggle": "파일 트리 토글", - "command.review.toggle": "검토 토글", - "command.terminal.new": "새 터미널", - "command.terminal.new.description": "새 터미널 탭 생성", - "command.steps.toggle": "단계 토글", - "command.steps.toggle.description": "현재 메시지의 단계 표시/숨기기", - "command.message.previous": "이전 메시지", - "command.message.previous.description": "이전 사용자 메시지로 이동", - "command.message.next": "다음 메시지", - "command.message.next.description": "다음 사용자 메시지로 이동", - "command.model.choose": "모델 선택", - "command.model.choose.description": "다른 모델 선택", - "command.mcp.toggle": "MCP 토글", - "command.mcp.toggle.description": "MCP 토글", - "command.agent.cycle": "에이전트 순환", - "command.agent.cycle.description": "다음 에이전트로 전환", - "command.agent.cycle.reverse": "에이전트 역순환", - "command.agent.cycle.reverse.description": "이전 에이전트로 전환", - "command.model.variant.cycle": "생각 수준 순환", - "command.model.variant.cycle.description": "다음 생각 수준으로 전환", - "command.prompt.mode.shell": "셸", - "command.prompt.mode.normal": "프롬프트", - "command.permissions.autoaccept.enable": "권한 자동 수락", - "command.permissions.autoaccept.disable": "권한 자동 수락 중지", - "command.workspace.toggle": "작업 공간 전환", - "command.workspace.toggle.description": "사이드바에서 다중 작업 공간 활성화 또는 비활성화", - "command.session.undo": "실행 취소", - "command.session.undo.description": "마지막 메시지 실행 취소", - "command.session.redo": "다시 실행", - "command.session.redo.description": "마지막 실행 취소된 메시지 다시 실행", - "command.session.compact": "세션 압축", - "command.session.compact.description": "컨텍스트 크기를 줄이기 위해 세션 요약", - "command.session.fork": "메시지에서 분기", - "command.session.fork.description": "이전 메시지에서 새 세션 생성", - "command.session.share": "세션 공유", - "command.session.share.description": "이 세션을 공유하고 URL을 클립보드에 복사", - "command.session.unshare": "세션 공유 중지", - "command.session.unshare.description": "이 세션 공유 중지", - "palette.search.placeholder": "파일, 명령어 및 세션 검색", - "palette.empty": "결과 없음", - "palette.group.commands": "명령어", - "palette.group.files": "파일", - "dialog.provider.search.placeholder": "공급자 검색", - "dialog.provider.empty": "공급자 없음", - "dialog.provider.group.popular": "인기", - "dialog.provider.group.other": "기타", - "dialog.provider.tag.recommended": "추천", - "dialog.provider.opencode.note": "Claude, GPT, Gemini 등을 포함한 엄선된 모델", - "dialog.provider.opencode.tagline": "신뢰할 수 있는 최적화 모델", - "dialog.provider.opencodeGo.tagline": "모두를 위한 저렴한 구독", - "dialog.provider.anthropic.note": "Claude Pro/Max 또는 API 키로 연결", - "dialog.provider.copilot.note": "Copilot 또는 API 키로 연결", - "dialog.provider.openai.note": "ChatGPT Pro/Plus 또는 API 키로 연결", - "dialog.provider.google.note": "빠르고 구조화된 응답을 위한 Gemini 모델", - "dialog.provider.openrouter.note": "모든 지원 모델을 단일 공급자에서 액세스", - "dialog.provider.vercel.note": "스마트 라우팅을 통한 AI 모델 통합 액세스", - "dialog.model.select.title": "모델 선택", - "dialog.model.search.placeholder": "모델 검색", - "dialog.model.empty": "모델 결과 없음", - "dialog.model.manage": "모델 관리", - "dialog.model.manage.description": "모델 선택기에 표시할 모델 사용자 지정", - "dialog.model.manage.provider.toggle": "모든 {{provider}} 모델 토글", - "dialog.model.unpaid.freeModels.title": "Kilo에서 제공하는 무료 모델", - "dialog.model.unpaid.addMore.title": "인기 공급자의 모델 추가", - "dialog.provider.viewAll": "더 많은 공급자 보기", - "provider.connect.title": "{{provider}} 연결", - "provider.connect.title.anthropicProMax": "Claude Pro/Max로 로그인", - "provider.connect.selectMethod": "{{provider}} 로그인 방법 선택", - "provider.connect.method.apiKey": "API 키", - "provider.connect.status.inProgress": "인증 진행 중...", - "provider.connect.status.waiting": "인증 대기 중...", - "provider.connect.status.failed": "인증 실패: {{error}}", - "provider.connect.apiKey.description": - "{{provider}} API 키를 입력하여 계정을 연결하고 Kilo에서 {{provider}} 모델을 사용하세요.", - "provider.connect.apiKey.label": "{{provider}} API 키", - "provider.connect.apiKey.placeholder": "API 키", - "provider.connect.apiKey.required": "API 키가 필요합니다", - "provider.connect.opencodeZen.line1": - "OpenCode Zen은 코딩 에이전트를 위해 최적화된 신뢰할 수 있는 엄선된 모델에 대한 액세스를 제공합니다.", - "provider.connect.opencodeZen.line2": "단일 API 키로 Claude, GPT, Gemini, GLM 등 다양한 모델에 액세스할 수 있습니다.", - "provider.connect.opencodeZen.visit.prefix": "다음 ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": "을 방문하여 API 키를 받으세요.", - "provider.connect.oauth.code.visit.prefix": "다음 ", - "provider.connect.oauth.code.visit.link": "이 링크", - "provider.connect.oauth.code.visit.suffix": - "를 방문하여 인증 코드를 받아 계정을 연결하고 Kilo에서 {{provider}} 모델을 사용하세요.", - "provider.connect.oauth.code.label": "{{method}} 인증 코드", - "provider.connect.oauth.code.placeholder": "인증 코드", - "provider.connect.oauth.code.required": "인증 코드가 필요합니다", - "provider.connect.oauth.code.invalid": "유효하지 않은 인증 코드", - "provider.connect.oauth.auto.visit.prefix": "다음 ", - "provider.connect.oauth.auto.visit.link": "이 링크", - "provider.connect.oauth.auto.visit.suffix": - "를 방문하고 아래 코드를 입력하여 계정을 연결하고 Kilo에서 {{provider}} 모델을 사용하세요.", - "provider.connect.oauth.auto.confirmationCode": "확인 코드", - "provider.connect.toast.connected.title": "{{provider}} 연결됨", - "provider.connect.toast.connected.description": "이제 {{provider}} 모델을 사용할 수 있습니다.", - "provider.custom.title": "사용자 지정 공급자", - "provider.custom.description.prefix": "OpenAI 호환 공급자를 구성합니다. ", - "provider.custom.description.link": "공급자 구성 문서", - "provider.custom.description.suffix": "를 참조하세요.", - "provider.custom.field.providerID.label": "공급자 ID", - "provider.custom.field.providerID.placeholder": "myprovider", - "provider.custom.field.providerID.description": "소문자, 숫자, 하이픈 또는 밑줄", - "provider.custom.field.name.label": "표시 이름", - "provider.custom.field.name.placeholder": "내 AI 공급자", - "provider.custom.field.baseURL.label": "기본 URL", - "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", - "provider.custom.field.apiKey.label": "API 키", - "provider.custom.field.apiKey.placeholder": "API 키", - "provider.custom.field.apiKey.description": "선택 사항입니다. 헤더를 통해 인증을 관리하는 경우 비워 두세요.", - "provider.custom.models.label": "모델", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "model-id", - "provider.custom.models.name.label": "이름", - "provider.custom.models.name.placeholder": "표시 이름", - "provider.custom.models.remove": "모델 제거", - "provider.custom.models.add": "모델 추가", - "provider.custom.headers.label": "헤더 (선택 사항)", - "provider.custom.headers.key.label": "헤더", - "provider.custom.headers.key.placeholder": "헤더 이름", - "provider.custom.headers.value.label": "값", - "provider.custom.headers.value.placeholder": "값", - "provider.custom.headers.remove": "헤더 제거", - "provider.custom.headers.add": "헤더 추가", - "provider.custom.error.providerID.required": "공급자 ID가 필요합니다", - "provider.custom.error.providerID.format": "소문자, 숫자, 하이픈 또는 밑줄을 사용하세요", - "provider.custom.error.providerID.exists": "해당 공급자 ID가 이미 존재합니다", - "provider.custom.error.name.required": "표시 이름이 필요합니다", - "provider.custom.error.baseURL.required": "기본 URL이 필요합니다", - "provider.custom.error.baseURL.format": "http:// 또는 https://로 시작해야 합니다", - "provider.custom.error.required": "필수", - "provider.custom.error.duplicate": "중복", - "provider.disconnect.toast.disconnected.title": "{{provider}} 연결 해제됨", - "provider.disconnect.toast.disconnected.description": "{{provider}} 모델을 더 이상 사용할 수 없습니다.", - "model.tag.free": "무료", - "model.tag.latest": "최신", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "텍스트", - "model.input.image": "이미지", - "model.input.audio": "오디오", - "model.input.video": "비디오", - "model.input.pdf": "pdf", - "model.tooltip.allows": "지원: {{inputs}}", - "model.tooltip.reasoning.allowed": "추론 허용", - "model.tooltip.reasoning.none": "추론 없음", - "model.tooltip.context": "컨텍스트 제한 {{limit}}", - "common.search.placeholder": "검색", - "common.goBack": "뒤로 가기", - "common.goForward": "앞으로 가기", - "common.loading": "로딩 중", - "common.loading.ellipsis": "...", - "common.cancel": "취소", - "common.connect": "연결", - "common.disconnect": "연결 해제", - "common.continue": "제출", - "common.submit": "제출", - "common.save": "저장", - "common.saving": "저장 중...", - "common.default": "기본값", - "common.attachment": "첨부 파일", - "prompt.placeholder.shell": "셸 명령어 입력... {{example}}", - "prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"', - "prompt.placeholder.simple": "무엇이든 물어보세요...", - "prompt.placeholder.summarizeComments": "댓글 요약…", - "prompt.placeholder.summarizeComment": "댓글 요약…", - "prompt.mode.shell": "셸", - "prompt.mode.normal": "프롬프트", - "prompt.mode.shell.exit": "종료하려면 esc", - "prompt.example.1": "코드베이스의 TODO 수정", - "prompt.example.2": "이 프로젝트의 기술 스택이 무엇인가요?", - "prompt.example.3": "고장 난 테스트 수정", - "prompt.example.4": "인증 작동 방식 설명", - "prompt.example.5": "보안 취약점 찾기 및 수정", - "prompt.example.6": "사용자 서비스에 단위 테스트 추가", - "prompt.example.7": "이 함수를 더 읽기 쉽게 리팩터링", - "prompt.example.8": "이 오류는 무엇을 의미하나요?", - "prompt.example.9": "이 문제 디버깅 도와줘", - "prompt.example.10": "API 문서 생성", - "prompt.example.11": "데이터베이스 쿼리 최적화", - "prompt.example.12": "입력 유효성 검사 추가", - "prompt.example.13": "...를 위한 새 컴포넌트 생성", - "prompt.example.14": "이 프로젝트를 어떻게 배포하나요?", - "prompt.example.15": "모범 사례를 기준으로 내 코드 검토", - "prompt.example.16": "이 함수에 오류 처리 추가", - "prompt.example.17": "이 정규식 패턴 설명", - "prompt.example.18": "이것을 TypeScript로 변환", - "prompt.example.19": "코드베이스 전체에 로깅 추가", - "prompt.example.20": "오래된 종속성은 무엇인가요?", - "prompt.example.21": "마이그레이션 스크립트 작성 도와줘", - "prompt.example.22": "이 엔드포인트에 캐싱 구현", - "prompt.example.23": "이 목록에 페이지네이션 추가", - "prompt.example.24": "...를 위한 CLI 명령어 생성", - "prompt.example.25": "여기서 환경 변수는 어떻게 작동하나요?", - "prompt.popover.emptyResults": "일치하는 결과 없음", - "prompt.popover.emptyCommands": "일치하는 명령어 없음", - "prompt.dropzone.label": "이미지, PDF 또는 텍스트 파일을 이곳에 드롭하세요", - "prompt.dropzone.file.label": "드롭하여 파일 @멘션 추가", - "prompt.slash.badge.custom": "사용자 지정", - "prompt.slash.badge.skill": "스킬", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "활성", - "prompt.context.includeActiveFile": "활성 파일 포함", - "prompt.context.removeActiveFile": "컨텍스트에서 활성 파일 제거", - "prompt.context.removeFile": "컨텍스트에서 파일 제거", - "prompt.action.attachFile": "파일 첨부", - "prompt.attachment.remove": "첨부 파일 제거", - "prompt.action.send": "전송", - "prompt.action.stop": "중지", - "prompt.toast.pasteUnsupported.title": "지원되지 않는 첨부 파일", - "prompt.toast.pasteUnsupported.description": "이미지, PDF 또는 텍스트 파일만 첨부할 수 있습니다.", - "prompt.toast.modelAgentRequired.title": "에이전트 및 모델 선택", - "prompt.toast.modelAgentRequired.description": "프롬프트를 보내기 전에 에이전트와 모델을 선택하세요.", - "prompt.toast.worktreeCreateFailed.title": "작업 트리 생성 실패", - "prompt.toast.sessionCreateFailed.title": "세션 생성 실패", - "prompt.toast.shellSendFailed.title": "셸 명령 전송 실패", - "prompt.toast.commandSendFailed.title": "명령 전송 실패", - "prompt.toast.promptSendFailed.title": "프롬프트 전송 실패", - "prompt.toast.promptSendFailed.description": "세션을 가져올 수 없습니다", - "dialog.mcp.title": "MCP", - "dialog.mcp.description": "{{total}}개 중 {{enabled}}개 활성화됨", - "dialog.mcp.empty": "구성된 MCP 없음", - "dialog.lsp.empty": "파일 유형에서 자동 감지된 LSP", - "dialog.plugins.empty": "opencode.json에 구성된 플러그인", - "mcp.status.connected": "연결됨", - "mcp.status.failed": "실패", - "mcp.status.needs_auth": "인증 필요", - "mcp.status.disabled": "비활성화됨", - "dialog.fork.empty": "분기할 메시지 없음", - "dialog.directory.search.placeholder": "폴더 검색", - "dialog.directory.empty": "폴더 없음", - "dialog.server.title": "서버", - "dialog.server.description": "이 앱이 연결할 Kilo 서버를 전환합니다.", - "dialog.server.search.placeholder": "서버 검색", - "dialog.server.empty": "서버 없음", - "dialog.server.add.title": "서버 추가", - "dialog.server.add.url": "서버 URL", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "서버에 연결할 수 없습니다", - "dialog.server.add.checking": "확인 중...", - "dialog.server.add.button": "서버 추가", - "dialog.server.add.name": "서버 이름 (선택 사항)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "사용자 이름 (선택 사항)", - "dialog.server.add.password": "비밀번호 (선택 사항)", - "dialog.server.edit.title": "서버 편집", - "dialog.server.default.title": "기본 서버", - "dialog.server.default.description": - "로컬 서버를 시작하는 대신 앱 실행 시 이 서버에 연결합니다. 다시 시작해야 합니다.", - "dialog.server.default.none": "선택된 서버 없음", - "dialog.server.default.set": "현재 서버를 기본값으로 설정", - "dialog.server.default.clear": "지우기", - "dialog.server.action.remove": "서버 제거", - "dialog.server.menu.edit": "편집", - "dialog.server.menu.default": "기본값으로 설정", - "dialog.server.menu.defaultRemove": "기본값 제거", - "dialog.server.menu.delete": "삭제", - "dialog.server.current": "현재 서버", - "dialog.server.status.default": "기본값", - "dialog.project.edit.title": "프로젝트 편집", - "dialog.project.edit.name": "이름", - "dialog.project.edit.icon": "아이콘", - "dialog.project.edit.icon.alt": "프로젝트 아이콘", - "dialog.project.edit.icon.hint": "이미지를 클릭하거나 드래그하세요", - "dialog.project.edit.icon.recommended": "권장: 128x128px", - "dialog.project.edit.color": "색상", - "dialog.project.edit.color.select": "{{color}} 색상 선택", - "dialog.project.edit.worktree.startup": "작업 공간 시작 스크립트", - "dialog.project.edit.worktree.startup.description": "새 작업 공간(작업 트리)을 만든 뒤 실행됩니다.", - "dialog.project.edit.worktree.startup.placeholder": "예: bun install", - "context.breakdown.title": "컨텍스트 분석", - "context.breakdown.note": '입력 토큰의 대략적인 분석입니다. "기타"에는 도구 정의 및 오버헤드가 포함됩니다.', - "context.breakdown.system": "시스템", - "context.breakdown.user": "사용자", - "context.breakdown.assistant": "어시스턴트", - "context.breakdown.tool": "도구 호출", - "context.breakdown.other": "기타", - "context.systemPrompt.title": "시스템 프롬프트", - "context.rawMessages.title": "원시 메시지", - "context.stats.session": "세션", - "context.stats.messages": "메시지", - "context.stats.provider": "공급자", - "context.stats.model": "모델", - "context.stats.limit": "컨텍스트 제한", - "context.stats.totalTokens": "총 토큰", - "context.stats.usage": "사용량", - "context.stats.inputTokens": "입력 토큰", - "context.stats.outputTokens": "출력 토큰", - "context.stats.reasoningTokens": "추론 토큰", - "context.stats.cacheTokens": "캐시 토큰 (읽기/쓰기)", - "context.stats.userMessages": "사용자 메시지", - "context.stats.assistantMessages": "어시스턴트 메시지", - "context.stats.totalCost": "총 비용", - "context.stats.sessionCreated": "세션 생성됨", - "context.stats.lastActivity": "최근 활동", - "context.usage.tokens": "토큰", - "context.usage.usage": "사용량", - "context.usage.cost": "비용", - "context.usage.clickToView": "컨텍스트를 보려면 클릭", - "context.usage.view": "컨텍스트 사용량 보기", - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - "toast.language.title": "언어", - "toast.language.description": "{{language}}(으)로 전환됨", - "toast.theme.title": "테마 전환됨", - "toast.scheme.title": "색상 테마", - "toast.workspace.enabled.title": "작업 공간 활성화됨", - "toast.workspace.enabled.description": "이제 사이드바에 여러 작업 트리가 표시됩니다", - "toast.workspace.disabled.title": "작업 공간 비활성화됨", - "toast.workspace.disabled.description": "사이드바에 메인 작업 트리만 표시됩니다", - "toast.permissions.autoaccept.on.title": "권한 자동 수락 중", - "toast.permissions.autoaccept.on.description": "권한 요청이 자동으로 승인됩니다", - "toast.permissions.autoaccept.off.title": "권한 자동 수락 중지됨", - "toast.permissions.autoaccept.off.description": "권한 요청에 승인이 필요합니다", - "toast.model.none.title": "선택된 모델 없음", - "toast.model.none.description": "이 세션을 요약하려면 공급자를 연결하세요", - "toast.file.loadFailed.title": "파일 로드 실패", - "toast.file.listFailed.title": "파일 목록을 불러오지 못했습니다", - "toast.context.noLineSelection.title": "줄 선택 없음", - "toast.context.noLineSelection.description": "먼저 파일 탭에서 줄 범위를 선택하세요.", - "toast.session.share.copyFailed.title": "URL 클립보드 복사 실패", - "toast.session.share.success.title": "세션 공유됨", - "toast.session.share.success.description": "공유 URL이 클립보드에 복사되었습니다!", - "toast.session.share.failed.title": "세션 공유 실패", - "toast.session.share.failed.description": "세션을 공유하는 동안 오류가 발생했습니다", - "toast.session.unshare.success.title": "세션 공유 해제됨", - "toast.session.unshare.success.description": "세션 공유가 성공적으로 해제되었습니다!", - "toast.session.unshare.failed.title": "세션 공유 해제 실패", - "toast.session.unshare.failed.description": "세션 공유를 해제하는 동안 오류가 발생했습니다", - "toast.session.listFailed.title": "{{project}}에 대한 세션을 로드하지 못했습니다", - "toast.update.title": "업데이트 가능", - "toast.update.description": "Kilo의 새 버전({{version}})을 설치할 수 있습니다.", - "toast.update.action.installRestart": "설치 및 다시 시작", - "toast.update.action.notYet": "나중에", - "error.page.title": "문제가 발생했습니다", - "error.page.description": "애플리케이션을 로드하는 동안 오류가 발생했습니다.", - "error.page.details.label": "오류 세부 정보", - "error.page.action.restart": "다시 시작", - "error.page.action.checking": "확인 중...", - "error.page.action.checkUpdates": "업데이트 확인", - "error.page.action.updateTo": "{{version}} 버전으로 업데이트", - "error.page.report.prefix": "이 오류를 Kilo 팀에 제보해 주세요: ", - "error.page.report.discord": "Discord", - "error.page.version": "버전: {{version}}", - "error.dev.rootNotFound": - "루트 요소를 찾을 수 없습니다. index.html에 추가하는 것을 잊으셨나요? 또는 id 속성의 철자가 틀렸을 수 있습니다.", - "error.globalSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?", - "directory.error.invalidUrl": "URL에 유효하지 않은 디렉터리가 있습니다.", - "error.chain.unknown": "알 수 없는 오류", - "error.chain.causedBy": "원인:", - "error.chain.apiError": "API 오류", - "error.chain.status": "상태: {{status}}", - "error.chain.retryable": "재시도 가능: {{retryable}}", - "error.chain.responseBody": "응답 본문:\n{{body}}", - "error.chain.didYouMean": "혹시 {{suggestions}}을(를) 의미하셨나요?", - "error.chain.modelNotFound": "모델을 찾을 수 없음: {{provider}}/{{model}}", - "error.chain.checkConfig": "구성(opencode.json)의 공급자/모델 이름을 확인하세요", - "error.chain.mcpFailed": 'MCP 서버 "{{name}}" 실패. 참고: Kilo는 아직 MCP 인증을 지원하지 않습니다.', - "error.chain.providerAuthFailed": "공급자 인증 실패 ({{provider}}): {{message}}", - "error.chain.providerInitFailed": '공급자 "{{provider}}" 초기화 실패. 자격 증명과 구성을 확인하세요.', - "error.chain.configJsonInvalid": "{{path}}의 구성 파일이 유효한 JSON(C)가 아닙니다", - "error.chain.configJsonInvalidWithMessage": "{{path}}의 구성 파일이 유효한 JSON(C)가 아닙니다: {{message}}", - "error.chain.configDirectoryTypo": - '{{path}}의 "{{dir}}" 디렉터리가 유효하지 않습니다. 디렉터리 이름을 "{{suggestion}}"으로 변경하거나 제거하세요. 이는 흔한 오타입니다.', - "error.chain.configFrontmatterError": "{{path}}의 frontmatter 파싱 실패:\n{{message}}", - "error.chain.configInvalid": "{{path}}의 구성 파일이 유효하지 않습니다", - "error.chain.configInvalidWithMessage": "{{path}}의 구성 파일이 유효하지 않습니다: {{message}}", - "notification.permission.title": "권한 필요", - "notification.permission.description": "{{projectName}}의 {{sessionTitle}}에서 권한이 필요합니다", - "notification.question.title": "질문", - "notification.question.description": "{{projectName}}의 {{sessionTitle}}에서 질문이 있습니다", - "notification.action.goToSession": "세션으로 이동", - "notification.session.responseReady.title": "응답 준비됨", - "notification.session.error.title": "세션 오류", - "notification.session.error.fallbackDescription": "오류가 발생했습니다", - "home.recentProjects": "최근 프로젝트", - "home.empty.title": "최근 프로젝트 없음", - "home.empty.description": "로컬 프로젝트를 열어 시작하세요", - "session.tab.session": "세션", - "session.tab.review": "검토", - "session.tab.context": "컨텍스트", - "session.panel.reviewAndFiles": "검토 및 파일", - "session.review.filesChanged": "{{count}}개 파일 변경됨", - "session.review.change.one": "변경", - "session.review.change.other": "변경", - "session.review.loadingChanges": "변경 사항 로드 중...", - "session.review.empty": "이 세션에 변경 사항이 아직 없습니다", - "session.review.noVcs": "Git 버전 관리 시스템이 감지되지 않아 변경 사항이 표시되지 않습니다", - "session.review.noSnapshot": "구성에서 스냅샷 추적이 비활성화되어 있어 세션 변경 사항을 사용할 수 없습니다", - "session.review.noChanges": "변경 없음", - "session.files.selectToOpen": "열 파일을 선택하세요", - "session.files.all": "모든 파일", - "session.files.empty": "파일 없음", - "session.files.binaryContent": "바이너리 파일 (내용을 표시할 수 없음)", - "session.messages.renderEarlier": "이전 메시지 렌더링", - "session.messages.loadingEarlier": "이전 메시지 로드 중...", - "session.messages.loadEarlier": "이전 메시지 로드", - "session.messages.loading": "메시지 로드 중...", - "session.messages.jumpToLatest": "최신으로 이동", - "session.context.addToContext": "컨텍스트에 {{selection}} 추가", - "session.todo.title": "할 일", - "session.todo.collapse": "접기", - "session.todo.expand": "펼치기", - "session.followupDock.summary.one": "{{count}}개의 대기 중인 메시지", - "session.followupDock.summary.other": "{{count}}개의 대기 중인 메시지", - "session.followupDock.sendNow": "지금 전송", - "session.followupDock.edit": "편집", - "session.followupDock.collapse": "대기 중인 메시지 접기", - "session.followupDock.expand": "대기 중인 메시지 펼치기", - "session.revertDock.summary.one": "{{count}}개의 롤백된 메시지", - "session.revertDock.summary.other": "{{count}}개의 롤백된 메시지", - "session.revertDock.collapse": "롤백된 메시지 접기", - "session.revertDock.expand": "롤백된 메시지 펼치기", - "session.revertDock.restore": "메시지 복원", - "session.new.title": "무엇이든 만들기", - "session.new.worktree.main": "메인 브랜치", - "session.new.worktree.mainWithBranch": "메인 브랜치 ({{branch}})", - "session.new.worktree.create": "새 작업 트리 생성", - "session.new.lastModified": "최근 수정", - "session.header.search.placeholder": "{{project}} 검색", - "session.header.searchFiles": "파일 검색", - "session.header.openIn": "다음에서 열기", - "session.header.open.action": "{{app}} 열기", - "session.header.open.ariaLabel": "{{app}}에서 열기", - "session.header.open.menu": "열기 옵션", - "session.header.open.copyPath": "경로 복사", - "status.popover.trigger": "상태", - "status.popover.ariaLabel": "서버 구성", - "status.popover.tab.servers": "서버", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "플러그인", - "status.popover.action.manageServers": "서버 관리", - "session.share.popover.title": "웹에 게시", - "session.share.popover.description.shared": "이 세션은 웹에 공개되었습니다. 링크가 있는 누구나 액세스할 수 있습니다.", - "session.share.popover.description.unshared": - "세션을 웹에 공개적으로 공유합니다. 링크가 있는 누구나 액세스할 수 있습니다.", - "session.share.action.share": "공유", - "session.share.action.publish": "게시", - "session.share.action.publishing": "게시 중...", - "session.share.action.unpublish": "게시 취소", - "session.share.action.unpublishing": "게시 취소 중...", - "session.share.action.view": "보기", - "session.share.copy.copied": "복사됨", - "session.share.copy.copyLink": "링크 복사", - "lsp.tooltip.none": "LSP 서버 없음", - "lsp.label.connected": "{{count}} LSP", - "prompt.loading": "프롬프트 로드 중...", - "terminal.loading": "터미널 로드 중...", - "terminal.title": "터미널", - "terminal.title.numbered": "터미널 {{number}}", - "terminal.close": "터미널 닫기", - "terminal.connectionLost.title": "연결 끊김", - "terminal.connectionLost.description": - "터미널 연결이 중단되었습니다. 서버가 재시작하면 이런 일이 발생할 수 있습니다.", - "common.closeTab": "탭 닫기", - "common.dismiss": "닫기", - "common.requestFailed": "요청 실패", - "common.moreOptions": "더 많은 옵션", - "common.learnMore": "더 알아보기", - "common.rename": "이름 바꾸기", - "common.reset": "초기화", - "common.archive": "보관", - "common.delete": "삭제", - "common.close": "닫기", - "common.edit": "편집", - "common.loadMore": "더 불러오기", - "common.key.esc": "ESC", - "sidebar.menu.toggle": "메뉴 토글", - "sidebar.nav.projectsAndSessions": "프로젝트 및 세션", - "sidebar.settings": "설정", - "sidebar.help": "도움말", - "sidebar.workspaces.enable": "작업 공간 활성화", - "sidebar.workspaces.disable": "작업 공간 비활성화", - "sidebar.gettingStarted.title": "시작하기", - "sidebar.gettingStarted.line1": "Kilo에는 무료 모델이 포함되어 있어 즉시 시작할 수 있습니다.", - "sidebar.gettingStarted.line2": "Claude, GPT, Gemini 등을 포함한 모델을 사용하려면 공급자를 연결하세요.", - "sidebar.project.recentSessions": "최근 세션", - "sidebar.project.viewAllSessions": "모든 세션 보기", - "sidebar.project.clearNotifications": "알림 지우기", - "app.name.desktop": "Kilo Desktop", - "settings.section.desktop": "데스크톱", - "settings.section.server": "서버", - "settings.tab.general": "일반", - "settings.tab.shortcuts": "단축키", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL 통합", - "settings.desktop.wsl.description": "Windows의 WSL 내부에서 Kilo 서버를 실행합니다.", - "settings.general.section.appearance": "모양", - "settings.general.section.notifications": "시스템 알림", - "settings.general.section.updates": "업데이트", - "settings.general.section.sounds": "효과음", - "settings.general.section.feed": "피드", - "settings.general.section.display": "디스플레이", - "settings.general.row.language.title": "언어", - "settings.general.row.language.description": "Kilo 표시 언어 변경", - "settings.general.row.appearance.title": "모양", - "settings.general.row.appearance.description": "기기에서 Kilo가 보이는 방식 사용자 지정", - "settings.general.row.colorScheme.title": "색상 테마", - "settings.general.row.colorScheme.description": "Kilo가 시스템, 라이트 또는 다크 테마를 따를지 선택하세요", - "settings.general.row.theme.title": "테마", - "settings.general.row.theme.description": "Kilo 테마 사용자 지정", - "settings.general.row.font.title": "코드 글꼴", - "settings.general.row.font.description": "코드 블록에 사용되는 글꼴을 사용자 지정", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "UI 글꼴", - "settings.general.row.uiFont.description": "인터페이스 전반에 사용되는 글꼴을 사용자 지정", - "settings.general.row.followup.title": "후속 조치 동작", - "settings.general.row.followup.description": "후속 프롬프트를 즉시 실행할지 대기열에 넣을지 선택하세요", - "settings.general.row.followup.option.queue": "대기열", - "settings.general.row.followup.option.steer": "조종", - "settings.general.row.reasoningSummaries.title": "추론 요약 표시", - "settings.general.row.reasoningSummaries.description": "타임라인에 모델 추론 요약 표시", - "settings.general.row.shellToolPartsExpanded.title": "shell 도구 파트 펼치기", - "settings.general.row.shellToolPartsExpanded.description": - "타임라인에서 기본적으로 shell 도구 파트를 펼친 상태로 표시합니다", - "settings.general.row.editToolPartsExpanded.title": "edit 도구 파트 펼치기", - "settings.general.row.editToolPartsExpanded.description": - "타임라인에서 기본적으로 edit, write, patch 도구 파트를 펼친 상태로 표시합니다", - "settings.general.row.showSessionProgressBar.title": "세션 진행 표시줄 표시", - "settings.general.row.showSessionProgressBar.description": - "에이전트가 작업 중일 때 세션 상단에 애니메이션 진행 표시줄을 표시합니다", - "settings.general.row.wayland.title": "네이티브 Wayland 사용", - "settings.general.row.wayland.description": "Wayland에서 X11 폴백을 비활성화합니다. 다시 시작해야 합니다.", - "settings.general.row.wayland.tooltip": - "혼합 주사율 모니터가 있는 Linux에서는 네이티브 Wayland가 더 안정적일 수 있습니다.", - "settings.general.row.releaseNotes.title": "릴리스 노트", - "settings.general.row.releaseNotes.description": "업데이트 후 '새 소식' 팝업 표시", - "settings.updates.row.startup.title": "시작 시 업데이트 확인", - "settings.updates.row.startup.description": "Kilo를 실행할 때 업데이트를 자동으로 확인합니다", - "settings.updates.row.check.title": "업데이트 확인", - "settings.updates.row.check.description": "업데이트를 수동으로 확인하고, 사용 가능하면 설치합니다", - "settings.updates.action.checkNow": "지금 확인", - "settings.updates.action.checking": "확인 중...", - "settings.updates.toast.latest.title": "최신 상태입니다", - "settings.updates.toast.latest.description": "현재 최신 버전의 Kilo를 사용 중입니다.", - "sound.option.none": "없음", - "sound.option.alert01": "알림 01", - "sound.option.alert02": "알림 02", - "sound.option.alert03": "알림 03", - "sound.option.alert04": "알림 04", - "sound.option.alert05": "알림 05", - "sound.option.alert06": "알림 06", - "sound.option.alert07": "알림 07", - "sound.option.alert08": "알림 08", - "sound.option.alert09": "알림 09", - "sound.option.alert10": "알림 10", - "sound.option.bipbop01": "빕-밥 01", - "sound.option.bipbop02": "빕-밥 02", - "sound.option.bipbop03": "빕-밥 03", - "sound.option.bipbop04": "빕-밥 04", - "sound.option.bipbop05": "빕-밥 05", - "sound.option.bipbop06": "빕-밥 06", - "sound.option.bipbop07": "빕-밥 07", - "sound.option.bipbop08": "빕-밥 08", - "sound.option.bipbop09": "빕-밥 09", - "sound.option.bipbop10": "빕-밥 10", - "sound.option.staplebops01": "스테이플밥스 01", - "sound.option.staplebops02": "스테이플밥스 02", - "sound.option.staplebops03": "스테이플밥스 03", - "sound.option.staplebops04": "스테이플밥스 04", - "sound.option.staplebops05": "스테이플밥스 05", - "sound.option.staplebops06": "스테이플밥스 06", - "sound.option.staplebops07": "스테이플밥스 07", - "sound.option.nope01": "아니오 01", - "sound.option.nope02": "아니오 02", - "sound.option.nope03": "아니오 03", - "sound.option.nope04": "아니오 04", - "sound.option.nope05": "아니오 05", - "sound.option.nope06": "아니오 06", - "sound.option.nope07": "아니오 07", - "sound.option.nope08": "아니오 08", - "sound.option.nope09": "아니오 09", - "sound.option.nope10": "아니오 10", - "sound.option.nope11": "아니오 11", - "sound.option.nope12": "아니오 12", - "sound.option.yup01": "네 01", - "sound.option.yup02": "네 02", - "sound.option.yup03": "네 03", - "sound.option.yup04": "네 04", - "sound.option.yup05": "네 05", - "sound.option.yup06": "네 06", - "settings.general.notifications.agent.title": "에이전트", - "settings.general.notifications.agent.description": "에이전트가 완료되거나 주의가 필요할 때 시스템 알림 표시", - "settings.general.notifications.permissions.title": "권한", - "settings.general.notifications.permissions.description": "권한이 필요할 때 시스템 알림 표시", - "settings.general.notifications.errors.title": "오류", - "settings.general.notifications.errors.description": "오류가 발생했을 때 시스템 알림 표시", - "settings.general.sounds.agent.title": "에이전트", - "settings.general.sounds.agent.description": "에이전트가 완료되거나 주의가 필요할 때 소리 재생", - "settings.general.sounds.permissions.title": "권한", - "settings.general.sounds.permissions.description": "권한이 필요할 때 소리 재생", - "settings.general.sounds.errors.title": "오류", - "settings.general.sounds.errors.description": "오류가 발생했을 때 소리 재생", - "settings.shortcuts.title": "키보드 단축키", - "settings.shortcuts.reset.button": "기본값으로 초기화", - "settings.shortcuts.reset.toast.title": "단축키 초기화됨", - "settings.shortcuts.reset.toast.description": "키보드 단축키가 기본값으로 초기화되었습니다.", - "settings.shortcuts.conflict.title": "단축키가 이미 사용 중임", - "settings.shortcuts.conflict.description": "{{keybind}}은(는) 이미 {{titles}}에 할당되어 있습니다.", - "settings.shortcuts.unassigned": "할당되지 않음", - "settings.shortcuts.pressKeys": "키 누르기", - "settings.shortcuts.search.placeholder": "단축키 검색", - "settings.shortcuts.search.empty": "단축키를 찾을 수 없습니다", - "settings.shortcuts.group.general": "일반", - "settings.shortcuts.group.session": "세션", - "settings.shortcuts.group.navigation": "탐색", - "settings.shortcuts.group.modelAndAgent": "모델 및 에이전트", - "settings.shortcuts.group.terminal": "터미널", - "settings.shortcuts.group.prompt": "프롬프트", - "settings.providers.title": "공급자", - "settings.providers.description": "공급자 설정은 여기서 구성할 수 있습니다.", - "settings.providers.section.connected": "연결된 공급자", - "settings.providers.connected.empty": "연결된 공급자 없음", - "settings.providers.section.popular": "인기 공급자", - "settings.providers.tag.environment": "환경", - "settings.providers.tag.config": "구성", - "settings.providers.tag.custom": "사용자 지정", - "settings.providers.tag.other": "기타", - "settings.models.title": "모델", - "settings.models.description": "모델 설정은 여기서 구성할 수 있습니다.", - "settings.agents.title": "에이전트", - "settings.agents.description": "에이전트 설정은 여기서 구성할 수 있습니다.", - "settings.commands.title": "명령어", - "settings.commands.description": "명령어 설정은 여기서 구성할 수 있습니다.", - "settings.mcp.title": "MCP", - "settings.mcp.description": "MCP 설정은 여기서 구성할 수 있습니다.", - "settings.permissions.title": "권한", - "settings.permissions.description": "서버가 기본적으로 사용할 수 있는 도구를 제어합니다.", - "settings.permissions.section.tools": "도구", - "settings.permissions.toast.updateFailed.title": "권한 업데이트 실패", - "settings.permissions.action.allow": "허용", - "settings.permissions.action.ask": "묻기", - "settings.permissions.action.deny": "거부", - "settings.permissions.tool.read.title": "읽기", - "settings.permissions.tool.read.description": "파일 읽기 (파일 경로와 일치)", - "settings.permissions.tool.edit.title": "편집", - "settings.permissions.tool.edit.description": "파일 수정 (편집, 쓰기, 패치 및 다중 편집 포함)", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "glob 패턴을 사용하여 파일 일치", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "정규식을 사용하여 파일 내용 검색", - "settings.permissions.tool.list.title": "목록", - "settings.permissions.tool.list.description": "디렉터리 내 파일 나열", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "셸 명령어 실행", - "settings.permissions.tool.task.title": "작업", - "settings.permissions.tool.task.description": "하위 에이전트 실행", - "settings.permissions.tool.skill.title": "기술", - "settings.permissions.tool.skill.description": "이름으로 기술 로드", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "언어 서버 쿼리 실행", - "settings.permissions.tool.todowrite.title": "할 일 쓰기", - "settings.permissions.tool.todowrite.description": "할 일 목록 업데이트", - "settings.permissions.tool.webfetch.title": "웹 가져오기", - "settings.permissions.tool.webfetch.description": "URL에서 콘텐츠 가져오기", - "settings.permissions.tool.websearch.title": "웹 검색", - "settings.permissions.tool.websearch.description": "웹 검색", - "settings.permissions.tool.codesearch.title": "코드 검색", - "settings.permissions.tool.codesearch.description": "웹에서 코드 검색", - "settings.permissions.tool.external_directory.title": "외부 디렉터리", - "settings.permissions.tool.external_directory.description": "프로젝트 디렉터리 외부의 파일에 액세스", - "settings.permissions.tool.doom_loop.title": "무한 반복", - "settings.permissions.tool.doom_loop.description": "동일한 입력으로 반복되는 도구 호출 감지", - "session.delete.failed.title": "세션 삭제 실패", - "session.delete.title": "세션 삭제", - "session.delete.confirm": '"{{name}}" 세션을 삭제하시겠습니까?', - "session.delete.button": "세션 삭제", - "workspace.new": "새 작업 공간", - "workspace.type.local": "로컬", - "workspace.type.sandbox": "샌드박스", - "workspace.create.failed.title": "작업 공간 생성 실패", - "workspace.delete.failed.title": "작업 공간 삭제 실패", - "workspace.resetting.title": "작업 공간 재설정 중", - "workspace.resetting.description": "잠시 시간이 걸릴 수 있습니다.", - "workspace.reset.failed.title": "작업 공간 재설정 실패", - "workspace.reset.success.title": "작업 공간 재설정됨", - "workspace.reset.success.description": "작업 공간이 이제 기본 브랜치와 일치합니다.", - "workspace.error.stillPreparing": "작업 공간이 아직 준비 중입니다", - "workspace.status.checking": "병합되지 않은 변경 사항 확인 중...", - "workspace.status.error": "Git 상태를 확인할 수 없습니다.", - "workspace.status.clean": "병합되지 않은 변경 사항이 감지되지 않았습니다.", - "workspace.status.dirty": "이 작업 공간에서 병합되지 않은 변경 사항이 감지되었습니다.", - "workspace.delete.title": "작업 공간 삭제", - "workspace.delete.confirm": '"{{name}}" 작업 공간을 삭제하시겠습니까?', - "workspace.delete.button": "작업 공간 삭제", - "workspace.reset.title": "작업 공간 재설정", - "workspace.reset.confirm": '"{{name}}" 작업 공간을 재설정하시겠습니까?', - "workspace.reset.button": "작업 공간 재설정", - "workspace.reset.archived.none": "활성 세션이 보관되지 않습니다.", - "workspace.reset.archived.one": "1개의 세션이 보관됩니다.", - "workspace.reset.archived.many": "{{count}}개의 세션이 보관됩니다.", - "workspace.reset.note": "이 작업은 작업 공간을 기본 브랜치와 일치하도록 재설정합니다.", - "common.open": "열기", - "dialog.releaseNotes.action.getStarted": "시작하기", - "dialog.releaseNotes.action.next": "다음", - "dialog.releaseNotes.action.hideFuture": "다시 보지 않기", - "dialog.releaseNotes.media.alt": "릴리스 미리보기", - "toast.project.reloadFailed.title": "{{project}} 다시 불러오기 실패", - "error.server.invalidConfiguration": "잘못된 구성", - "common.moreCountSuffix": " (외 {{count}}개)", - "common.time.justNow": "방금 전", - "common.time.minutesAgo.short": "{{count}}분 전", - "common.time.hoursAgo.short": "{{count}}시간 전", - "common.time.daysAgo.short": "{{count}}일 전", - "settings.providers.connected.environmentDescription": "환경 변수에서 연결됨", - "settings.providers.custom.description": "기본 URL로 OpenAI 호환 공급자를 추가합니다.", - - "app.server.unreachable": "{{server}}에 연결할 수 없습니다", - "app.server.retrying": "자동으로 재시도 중...", - "app.server.otherServers": "다른 서버", - "dialog.server.add.usernamePlaceholder": "사용자 이름", - "dialog.server.add.passwordPlaceholder": "비밀번호", - "server.row.noUsername": "사용자 이름 없음", - "session.review.noVcs.createGit.title": "Git 저장소 생성", - "session.review.noVcs.createGit.description": "이 프로젝트의 변경 사항을 추적, 검토 및 실행 취소", - "session.review.noVcs.createGit.actionLoading": "Git 저장소 생성 중...", - "session.review.noVcs.createGit.action": "Git 저장소 생성", - "session.todo.progress": "{{total}}개의 할 일 중 {{done}}개 완료", - "session.question.progress": "{{total}}개의 질문 중 {{current}}개", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "파일 탐색기", - "session.header.open.fileManager": "파일 관리자", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "터미널", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "개발 성능 진단", - "debugBar.na": "해당 없음", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": - "세션 페이지에 닿은 마지막 완료된 라우트 전환. 라우터 시작부터 정착 후 첫 번째 페인트까지 측정됨.", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "지난 5초간의 초당 프레임 수.", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "지난 5초간의 최악의 프레임 시간.", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "지난 5초간 32ms를 초과한 프레임.", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "지난 5초간의 차단된 시간 및 긴 작업 수. 최대 작업: {{max}}.", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "지난 5초간 관찰된 최악의 입력 지연.", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": "지난 5초간의 대략적인 상호작용 지속 시간. 이것은 공식 Web Vitals INP가 아닌 INP와 유사합니다.", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "현재 앱 수명 동안의 누적 레이아웃 이동.", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "사용된 JS 힙 대 힙 제한. Chromium 전용.", - "debugBar.mem.tip": "사용된 JS 힙 대 힙 제한. {{limit}} 중 {{used}}.", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "Space", - "common.key.backspace": "Backspace", - "common.key.enter": "Enter", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "알 수 없음", - "error.page.circular": "[순환]", - "error.globalSDK.noServerAvailable": "사용 가능한 서버 없음", - "error.globalSDK.serverNotAvailable": "서버를 사용할 수 없음", - "error.childStore.persistedCacheCreateFailed": "영구 캐시 생성 실패", - "error.childStore.persistedProjectMetadataCreateFailed": "영구 프로젝트 메타데이터 생성 실패", - "error.childStore.persistedProjectIconCreateFailed": "영구 프로젝트 아이콘 생성 실패", - "error.childStore.storeCreateFailed": "저장소 생성 실패", - "terminal.connectionLost.abnormalClose": "WebSocket이 비정상적으로 닫힘: {{code}}", -} diff --git a/packages/app/src/i18n/nl.ts b/packages/app/src/i18n/nl.ts deleted file mode 100644 index 376aaa42bbf..00000000000 --- a/packages/app/src/i18n/nl.ts +++ /dev/null @@ -1,855 +0,0 @@ -export const dict = { - "command.category.suggested": "Voorgesteld", - "command.category.view": "Weergave", - "command.category.project": "Project", - "command.category.provider": "Provider", - "command.category.server": "Server", - "command.category.session": "Sessie", - "command.category.theme": "Thema", - "command.category.language": "Taal", - "command.category.file": "Bestand", - "command.category.context": "Context", - "command.category.terminal": "Terminal", - "command.category.model": "Model", - "command.category.mcp": "MCP", - "command.category.agent": "Agent", - "command.category.permissions": "Machtigingen", - "command.category.workspace": "Werkruimte", - "command.category.settings": "Instellingen", - - "theme.scheme.system": "Systeem", - "theme.scheme.light": "Licht", - "theme.scheme.dark": "Donker", - - "command.sidebar.toggle": "Zijbalk in-/uitschakelen", - "command.project.open": "Project openen", - "command.provider.connect": "Provider verbinden", - "command.server.switch": "Server wisselen", - "command.settings.open": "Instellingen openen", - "command.session.previous": "Vorige sessie", - "command.session.next": "Volgende sessie", - "command.session.previous.unseen": "Vorige ongelezen sessie", - "command.session.next.unseen": "Volgende ongelezen sessie", - "command.session.archive": "Sessie archiveren", - - "command.palette": "Opdrachtenpalet", - - "command.theme.cycle": "Volgend thema", - "command.theme.set": "Gebruik thema: {{theme}}", - "command.theme.scheme.cycle": "Volgend kleurenschema", - "command.theme.scheme.set": "Gebruik kleurenschema: {{scheme}}", - - "command.language.cycle": "Volgende taal", - "command.language.set": "Gebruik taal: {{language}}", - - "command.session.new": "Nieuwe sessie", - "command.file.open": "Bestand openen", - "command.tab.close": "Tabblad sluiten", - "command.context.addSelection": "Selectie toevoegen aan context", - "command.context.addSelection.description": "Geselecteerde regels van het huidige bestand toevoegen", - "command.input.focus": "Invoer focussen", - "command.terminal.toggle": "Terminal in-/uitschakelen", - "command.fileTree.toggle": "Bestandsboom in-/uitschakelen", - "command.review.toggle": "Review in-/uitschakelen", - "command.terminal.new": "Nieuwe terminal", - "command.terminal.new.description": "Een nieuw terminal-tabblad maken", - "command.steps.toggle": "Stappen in-/uitschakelen", - "command.steps.toggle.description": "Stappen voor het huidige bericht tonen of verbergen", - "command.message.previous": "Vorig bericht", - "command.message.previous.description": "Ga naar het vorige gebruikersbericht", - "command.message.next": "Volgend bericht", - "command.message.next.description": "Ga naar het volgende gebruikersbericht", - "command.model.choose": "Model kiezen", - "command.model.choose.description": "Selecteer een ander model", - "command.mcp.toggle": "MCPs in-/uitschakelen", - "command.mcp.toggle.description": "MCPs in-/uitschakelen", - "command.agent.cycle": "Volgende agent", - "command.agent.cycle.description": "Overschakelen naar de volgende agent", - "command.agent.cycle.reverse": "Vorige agent", - "command.agent.cycle.reverse.description": "Overschakelen naar de vorige agent", - "command.model.variant.cycle": "Volgende denkinspanning", - "command.model.variant.cycle.description": "Overschakelen naar het volgende inspanningsniveau", - "command.prompt.mode.shell": "Shell", - "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Machtigingen automatisch accepteren", - "command.permissions.autoaccept.disable": "Machtigingen niet langer automatisch accepteren", - "command.workspace.toggle": "Werkruimtes in-/uitschakelen", - "command.workspace.toggle.description": "Meerdere werkruimtes in de zijbalk in- of uitschakelen", - "command.session.undo": "Ongedaan maken", - "command.session.undo.description": "Laatste bericht ongedaan maken", - "command.session.redo": "Opnieuw uitvoeren", - "command.session.redo.description": "Het laatst ongedaan gemaakte bericht opnieuw uitvoeren", - "command.session.compact": "Sessie comprimeren", - "command.session.compact.description": "Sessie samenvatten om contextgrootte te verkleinen", - "command.session.fork": "Afsplitsen van bericht", - "command.session.fork.description": "Nieuwe sessie maken van een eerder bericht", - "command.session.share": "Sessie delen", - "command.session.share.description": "Deze sessie delen en de URL naar klembord kopiëren", - "command.session.unshare": "Sessie niet meer delen", - "command.session.unshare.description": "Stoppen met het delen van deze sessie", - - "palette.search.placeholder": "Zoek bestanden, opdrachten en sessies", - "palette.empty": "Geen resultaten gevonden", - "palette.group.commands": "Opdrachten", - "palette.group.files": "Bestanden", - - "dialog.provider.search.placeholder": "Zoek providers", - "dialog.provider.empty": "Geen providers gevonden", - "dialog.provider.group.popular": "Populair", - "dialog.provider.group.other": "Overig", - "dialog.provider.tag.recommended": "Aanbevolen", - "dialog.provider.opencode.note": "Geselecteerde modellen inclusief Claude, GPT, Gemini en meer", - "dialog.provider.opencode.tagline": "Betrouwbare geoptimaliseerde modellen", - "dialog.provider.opencodeGo.tagline": "Goedkoop abonnement voor iedereen", - "dialog.provider.anthropic.note": "Directe toegang tot Claude modellen, inclusief Pro en Max", - "dialog.provider.copilot.note": "AI-modellen voor codeerassistentie via GitHub Copilot", - "dialog.provider.openai.note": "GPT modellen voor snelle, capabele algemene AI-taken", - "dialog.provider.google.note": "Gemini modellen voor snelle, gestructureerde antwoorden", - "dialog.provider.openrouter.note": "Toegang tot alle ondersteunde modellen via één provider", - "dialog.provider.vercel.note": "Geünificeerde toegang tot AI-modellen met slimme routering", - - "dialog.model.select.title": "Selecteer model", - "dialog.model.search.placeholder": "Zoek modellen", - "dialog.model.empty": "Geen modelresultaten", - "dialog.model.manage": "Modellen beheren", - "dialog.model.manage.description": "Pas aan welke modellen in de modelkiezer verschijnen.", - "dialog.model.manage.provider.toggle": "Alle {{provider}} modellen in-/uitschakelen", - - "dialog.model.unpaid.freeModels.title": "Gratis modellen aangeboden door Kilo", - "dialog.model.unpaid.addMore.title": "Meer modellen toevoegen van populaire providers", - - "dialog.provider.viewAll": "Toon meer providers", - - "provider.connect.title": "{{provider}} verbinden", - "provider.connect.title.anthropicProMax": "Inloggen met Claude Pro/Max", - "provider.connect.selectMethod": "Selecteer inlogmethode voor {{provider}}.", - "provider.connect.method.apiKey": "API key", - "provider.connect.status.inProgress": "Autorisatie in uitvoering...", - "provider.connect.status.waiting": "Wachten op autorisatie...", - "provider.connect.status.failed": "Autorisatie mislukt: {{error}}", - "provider.connect.apiKey.description": - "Voer uw {{provider}} API key in om uw account te verbinden en {{provider}} modellen te gebruiken in Kilo.", - "provider.connect.apiKey.label": "{{provider}} API key", - "provider.connect.apiKey.placeholder": "API key", - "provider.connect.apiKey.required": "API key is vereist", - "provider.connect.opencodeZen.line1": - "OpenCode Zen geeft u toegang tot een geselecteerde set betrouwbare en geoptimaliseerde modellen voor coderingsagenten.", - "provider.connect.opencodeZen.line2": - "Met een enkele API key krijgt u toegang tot modellen zoals Claude, GPT, Gemini, GLM en meer.", - "provider.connect.opencodeZen.visit.prefix": "Bezoek ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " om uw API key te verkrijgen.", - "provider.connect.oauth.code.visit.prefix": "Bezoek ", - "provider.connect.oauth.code.visit.link": "deze link", - "provider.connect.oauth.code.visit.suffix": - " om uw autorisatiecode te verkrijgen, uw account te verbinden en {{provider}} modellen te gebruiken in Kilo.", - "provider.connect.oauth.code.label": "{{method}} autorisatiecode", - "provider.connect.oauth.code.placeholder": "Autorisatiecode", - "provider.connect.oauth.code.required": "Autorisatiecode is vereist", - "provider.connect.oauth.code.invalid": "Ongeldige autorisatiecode", - "provider.connect.oauth.auto.visit.prefix": "Bezoek ", - "provider.connect.oauth.auto.visit.link": "deze link", - "provider.connect.oauth.auto.visit.suffix": - " en voer de onderstaande code in om uw account te verbinden en {{provider}} modellen te gebruiken in Kilo.", - "provider.connect.oauth.auto.confirmationCode": "Bevestigingscode", - "provider.connect.toast.connected.title": "{{provider}} verbonden", - "provider.connect.toast.connected.description": "{{provider}} modellen zijn nu beschikbaar om te gebruiken.", - - "provider.custom.title": "Aangepaste provider", - "provider.custom.description.prefix": "Configureer een OpenAI-compatibele provider. Zie de ", - "provider.custom.description.link": "provider configuratiedocumentatie", - "provider.custom.description.suffix": ".", - "provider.custom.field.providerID.label": "Provider ID", - "provider.custom.field.providerID.placeholder": "myprovider", - "provider.custom.field.providerID.description": "Kleine letters, cijfers, koppeltekens of underscores", - "provider.custom.field.name.label": "Weergavenaam", - "provider.custom.field.name.placeholder": "Mijn AI Provider", - "provider.custom.field.baseURL.label": "Basis-URL", - "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", - "provider.custom.field.apiKey.label": "API key", - "provider.custom.field.apiKey.placeholder": "API key", - "provider.custom.field.apiKey.description": "Optioneel. Laat leeg als u auth via headers beheert.", - "provider.custom.models.label": "Modellen", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "model-id", - "provider.custom.models.name.label": "Naam", - "provider.custom.models.name.placeholder": "Weergavenaam", - "provider.custom.models.remove": "Model verwijderen", - "provider.custom.models.add": "Model toevoegen", - "provider.custom.headers.label": "Headers (optioneel)", - "provider.custom.headers.key.label": "Header", - "provider.custom.headers.key.placeholder": "Header-Naam", - "provider.custom.headers.value.label": "Waarde", - "provider.custom.headers.value.placeholder": "waarde", - "provider.custom.headers.remove": "Header verwijderen", - "provider.custom.headers.add": "Header toevoegen", - "provider.custom.error.providerID.required": "Provider ID is vereist", - "provider.custom.error.providerID.format": "Gebruik kleine letters, cijfers, koppeltekens of underscores", - "provider.custom.error.providerID.exists": "Deze provider ID bestaat al", - "provider.custom.error.name.required": "Weergavenaam is vereist", - "provider.custom.error.baseURL.required": "Basis-URL is vereist", - "provider.custom.error.baseURL.format": "Moet beginnen met http:// of https://", - "provider.custom.error.required": "Vereist", - "provider.custom.error.duplicate": "Duplicaat", - - "provider.disconnect.toast.disconnected.title": "{{provider}} losgekoppeld", - "provider.disconnect.toast.disconnected.description": "{{provider}} modellen zijn niet langer beschikbaar.", - - "model.tag.free": "Gratis", - "model.tag.latest": "Nieuwste", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "tekst", - "model.input.image": "afbeelding", - "model.input.audio": "audio", - "model.input.video": "video", - "model.input.pdf": "pdf", - "model.tooltip.allows": "Staat toe: {{inputs}}", - "model.tooltip.reasoning.allowed": "Staat redenering toe", - "model.tooltip.reasoning.none": "Geen redenering", - "model.tooltip.context": "Contextlimiet {{limit}}", - - "common.search.placeholder": "Zoeken", - "common.goBack": "Ga terug", - "common.goForward": "Ga vooruit", - "common.loading": "Laden", - "common.loading.ellipsis": "...", - "common.cancel": "Annuleren", - "common.open": "Openen", - "common.connect": "Verbinden", - "common.disconnect": "Loskoppelen", - "common.submit": "Verzenden", - "common.save": "Opslaan", - "common.saving": "Opslaan...", - "common.default": "Standaard", - "common.attachment": "bijlage", - - "prompt.placeholder.shell": "Voer shell-opdracht in...", - "prompt.placeholder.normal": 'Vraag alles... "{{example}}"', - "prompt.placeholder.simple": "Vraag alles...", - "prompt.placeholder.summarizeComments": "Reacties samenvatten...", - "prompt.placeholder.summarizeComment": "Reactie samenvatten...", - "prompt.mode.shell": "Shell", - "prompt.mode.normal": "Prompt", - "prompt.mode.shell.exit": "esc om te verlaten", - - "prompt.example.1": "Fix een TODO in de codebase", - "prompt.example.2": "Wat is de tech stack van dit project?", - "prompt.example.3": "Repareer kapotte tests", - "prompt.example.4": "Leg uit hoe authenticatie werkt", - "prompt.example.5": "Vind en herstel beveiligingskwetsbaarheden", - "prompt.example.6": "Voeg unittests toe voor de gebruikersservice", - "prompt.example.7": "Herschrijf deze functie om deze beter leesbaar te maken", - "prompt.example.8": "Wat betekent deze fout?", - "prompt.example.9": "Help me met het debuggen van dit probleem", - "prompt.example.10": "Genereer API-documentatie", - "prompt.example.11": "Optimaliseer databasequery's", - "prompt.example.12": "Voeg invoervalidatie toe", - "prompt.example.13": "Maak een nieuwe component voor...", - "prompt.example.14": "Hoe implementeer ik dit project?", - "prompt.example.15": "Bekijk mijn code voor best practices", - "prompt.example.16": "Voeg foutafhandeling toe aan deze functie", - "prompt.example.17": "Leg dit regex-patroon uit", - "prompt.example.18": "Zet dit om naar TypeScript", - "prompt.example.19": "Voeg logging toe in de hele codebase", - "prompt.example.20": "Welke afhankelijkheden zijn verouderd?", - "prompt.example.21": "Help me een migratiescript te schrijven", - "prompt.example.22": "Implementeer caching voor dit eindpunt", - "prompt.example.23": "Voeg paginering toe aan deze lijst", - "prompt.example.24": "Maak een CLI-opdracht voor...", - "prompt.example.25": "Hoe werken omgevingsvariabelen hier?", - - "prompt.popover.emptyResults": "Geen overeenkomende resultaten", - "prompt.popover.emptyCommands": "Geen overeenkomende opdrachten", - "prompt.dropzone.label": "Sleep afbeeldingen of PDF's hierheen", - "prompt.dropzone.file.label": "Sleep om bestand te @vermelden", - "prompt.slash.badge.custom": "aangepast", - "prompt.slash.badge.skill": "skill", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "actief", - "prompt.context.includeActiveFile": "Neem actief bestand op", - "prompt.context.removeActiveFile": "Verwijder actief bestand uit context", - "prompt.context.removeFile": "Verwijder bestand uit context", - "prompt.action.attachFile": "Bestand toevoegen", - "prompt.attachment.remove": "Bijlage verwijderen", - "prompt.action.send": "Verzenden", - "prompt.action.stop": "Stop", - - "prompt.toast.pasteUnsupported.title": "Niet-ondersteunde plakbewerking", - "prompt.toast.pasteUnsupported.description": "Hier kunnen alleen afbeeldingen of PDF's worden geplakt.", - "prompt.toast.modelAgentRequired.title": "Selecteer een agent en model", - "prompt.toast.modelAgentRequired.description": "Kies een agent en model voordat u een prompt verzendt.", - "prompt.toast.worktreeCreateFailed.title": "Maken van worktree mislukt", - "prompt.toast.sessionCreateFailed.title": "Maken van sessie mislukt", - "prompt.toast.shellSendFailed.title": "Verzenden van shell-opdracht mislukt", - "prompt.toast.commandSendFailed.title": "Verzenden van opdracht mislukt", - "prompt.toast.promptSendFailed.title": "Verzenden van prompt mislukt", - "prompt.toast.promptSendFailed.description": "Kan sessie niet ophalen", - - "dialog.mcp.title": "MCPs", - "dialog.mcp.description": "{{enabled}} van {{total}} ingeschakeld", - "dialog.mcp.empty": "Geen MCP's geconfigureerd", - - "dialog.lsp.empty": "LSP's automatisch gedetecteerd aan de hand van bestandstypen", - "dialog.plugins.empty": "Plug-ins geconfigureerd in opencode.json", - - "mcp.status.connected": "verbonden", - "mcp.status.failed": "mislukt", - "mcp.status.needs_auth": "authenticatie vereist", - "mcp.status.disabled": "uitgeschakeld", - - "dialog.fork.empty": "Geen berichten om van af te splitsen", - - "dialog.directory.search.placeholder": "Zoek mappen", - "dialog.directory.empty": "Geen mappen gevonden", - - "dialog.server.title": "Servers", - "dialog.server.description": "Wissel met welke Kilo server deze app verbindt.", - "dialog.server.search.placeholder": "Zoek servers", - "dialog.server.empty": "Nog geen servers", - "dialog.server.add.title": "Server toevoegen", - "dialog.server.add.url": "Serveradres", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "Kan geen verbinding maken met server", - "dialog.server.add.checking": "Controleren...", - "dialog.server.add.button": "Server toevoegen", - "dialog.server.add.name": "Servernaam (optioneel)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "Gebruikersnaam (optioneel)", - "dialog.server.add.password": "Wachtwoord (optioneel)", - "dialog.server.edit.title": "Server bewerken", - "dialog.server.default.title": "Standaardserver", - "dialog.server.default.description": - "Maak verbinding met deze server bij de lancering van de app in plaats van een lokale server te starten. Vereist een herstart.", - "dialog.server.default.none": "Geen server geselecteerd", - "dialog.server.default.set": "Huidige server als standaard instellen", - "dialog.server.default.clear": "Wissen", - "dialog.server.action.remove": "Server verwijderen", - - "dialog.server.menu.edit": "Bewerken", - "dialog.server.menu.default": "Als standaard instellen", - "dialog.server.menu.defaultRemove": "Standaard verwijderen", - "dialog.server.menu.delete": "Verwijderen", - "dialog.server.current": "Huidige Server", - "dialog.server.status.default": "Standaard", - - "dialog.project.edit.title": "Project bewerken", - "dialog.project.edit.name": "Naam", - "dialog.project.edit.icon": "Icoon", - "dialog.project.edit.icon.alt": "Projecticoon", - "dialog.project.edit.icon.hint": "Klik of sleep een afbeelding", - "dialog.project.edit.icon.recommended": "Aanbevolen: 128x128px", - "dialog.project.edit.color": "Kleur", - "dialog.project.edit.color.select": "Selecteer {{color}} kleur", - "dialog.project.edit.worktree.startup": "Werkruimte opstartscript", - "dialog.project.edit.worktree.startup.description": - "Wordt uitgevoerd na het maken van een nieuwe werkruimte (worktree).", - "dialog.project.edit.worktree.startup.placeholder": "bijv. bun install", - - "dialog.releaseNotes.action.getStarted": "Aan de slag", - "dialog.releaseNotes.action.next": "Volgende", - "dialog.releaseNotes.action.hideFuture": "Toon deze niet in de toekomst", - "dialog.releaseNotes.media.alt": "Releasevoorbeeld", - - "context.breakdown.title": "Contextverdeling", - "context.breakdown.note": 'Geschatte verdeling van invoertokens. "Overig" bevat tooldefinities en overhead.', - "context.breakdown.system": "Systeem", - "context.breakdown.user": "Gebruiker", - "context.breakdown.assistant": "Assistent", - "context.breakdown.tool": "Tool-aanroepen", - "context.breakdown.other": "Overig", - - "context.systemPrompt.title": "Systeemprompt", - "context.rawMessages.title": "Ruwe berichten", - - "context.stats.session": "Sessie", - "context.stats.messages": "Berichten", - "context.stats.provider": "Provider", - "context.stats.model": "Model", - "context.stats.limit": "Contextlimiet", - "context.stats.totalTokens": "Totaal Tokens", - "context.stats.usage": "Gebruik", - "context.stats.inputTokens": "Invoertokens", - "context.stats.outputTokens": "Uitvoertokens", - "context.stats.reasoningTokens": "Redeneertokens", - "context.stats.cacheTokens": "Cachetokens (lezen/schrijven)", - "context.stats.userMessages": "Gebruikersberichten", - "context.stats.assistantMessages": "Assistentberichten", - "context.stats.totalCost": "Totale Kosten", - "context.stats.sessionCreated": "Sessie Aangemaakt", - "context.stats.lastActivity": "Laatste Activiteit", - - "context.usage.tokens": "Tokens", - "context.usage.usage": "Gebruik", - "context.usage.cost": "Kosten", - "context.usage.clickToView": "Klik om context te bekijken", - "context.usage.view": "Contextgebruik bekijken", - - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.nl": "Nederlands", - "language.th": "ไทย", - "language.tr": "Türkçe", - - "toast.language.title": "Taal", - "toast.language.description": "Omgeschakeld naar {{language}}", - - "toast.theme.title": "Thema gewisseld", - "toast.scheme.title": "Kleurenschema", - - "toast.workspace.enabled.title": "Werkruimtes ingeschakeld", - "toast.workspace.enabled.description": "Meerdere worktrees worden nu weergegeven in de zijbalk", - "toast.workspace.disabled.title": "Werkruimtes uitgeschakeld", - "toast.workspace.disabled.description": "Alleen de hoofdworktree wordt weergegeven in de zijbalk", - - "toast.permissions.autoaccept.on.title": "Machtigingen automatisch accepteren", - "toast.permissions.autoaccept.on.description": "Toestemmingsverzoeken worden automatisch goedgekeurd", - "toast.permissions.autoaccept.off.title": "Gestopt met automatisch accepteren van machtigingen", - "toast.permissions.autoaccept.off.description": "Toestemmingsverzoeken vereisen goedkeuring", - - "toast.model.none.title": "Geen model geselecteerd", - "toast.model.none.description": "Verbind een provider om deze sessie samen te vatten", - - "toast.file.loadFailed.title": "Laden van bestand mislukt", - "toast.file.listFailed.title": "Lijst met bestanden ophalen mislukt", - - "toast.context.noLineSelection.title": "Geen regelselectie", - "toast.context.noLineSelection.description": "Selecteer eerst een regelbereik in een bestandstabblad.", - - "toast.session.share.copyFailed.title": "Kopiëren van URL naar klembord mislukt", - "toast.session.share.success.title": "Sessie gedeeld", - "toast.session.share.success.description": "Deel-URL naar klembord gekopieerd!", - "toast.session.share.failed.title": "Delen van sessie mislukt", - "toast.session.share.failed.description": "Er is een fout opgetreden bij het delen van de sessie", - - "toast.session.unshare.success.title": "Sessie niet meer gedeeld", - "toast.session.unshare.success.description": "Sessie succesvol niet meer gedeeld!", - "toast.session.unshare.failed.title": "Sessie niet meer delen mislukt", - "toast.session.unshare.failed.description": - "Er is een fout opgetreden bij het ongedaan maken van delen van de sessie", - - "toast.session.listFailed.title": "Laden van sessies voor {{project}} mislukt", - "toast.project.reloadFailed.title": "Herladen van {{project}} mislukt", - - "toast.update.title": "Update beschikbaar", - "toast.update.description": "Een nieuwe versie van Kilo ({{version}}) is nu beschikbaar om te installeren.", - "toast.update.action.installRestart": "Installeren en herstarten", - "toast.update.action.notYet": "Nog niet", - - "error.page.title": "Er is iets misgegaan", - "error.page.description": "Er is een fout opgetreden tijdens het laden van de applicatie.", - "error.page.details.label": "Foutdetails", - "error.page.action.restart": "Herstarten", - "error.page.action.checking": "Controleren...", - "error.page.action.checkUpdates": "Controleren op updates", - "error.page.action.updateTo": "Updaten naar {{version}}", - "error.page.report.prefix": "Meld deze fout alstublieft aan het Kilo team", - "error.page.report.discord": "op Discord", - "error.page.version": "Versie: {{version}}", - - "error.dev.rootNotFound": - "Root-element niet gevonden. Bent u vergeten dit toe te voegen aan uw index.html? Of misschien is het id-attribuut verkeerd gespeld?", - - "error.globalSync.connectFailed": "Kon geen verbinding maken met server. Draait er een server op `{{url}}`?", - "directory.error.invalidUrl": "Ongeldige map in URL.", - - "error.chain.unknown": "Onbekende fout", - "error.server.invalidConfiguration": "Ongeldige configuratie", - "error.chain.causedBy": "Veroorzaakt door:", - "error.chain.apiError": "API-fout", - "error.chain.status": "Status: {{status}}", - "error.chain.retryable": "Opnieuw te proberen: {{retryable}}", - "error.chain.responseBody": "Reactie-body:\n{{body}}", - "error.chain.didYouMean": "Bedoelde u: {{suggestions}}", - "error.chain.modelNotFound": "Model niet gevonden: {{provider}}/{{model}}", - "error.chain.checkConfig": "Controleer uw config (opencode.json) provider/modelnamen", - "error.chain.mcpFailed": 'MCP-server "{{name}}" is mislukt. Let op, Kilo ondersteunt nog geen MCP-authenticatie.', - "error.chain.providerAuthFailed": "Providerauthenticatie mislukt ({{provider}}): {{message}}", - "error.chain.providerInitFailed": - 'Initialiseren van provider "{{provider}}" mislukt. Controleer referenties en configuratie.', - "error.chain.configJsonInvalid": "Configuratiebestand op {{path}} is geen geldige JSON(C)", - "error.chain.configJsonInvalidWithMessage": "Configuratiebestand op {{path}} is geen geldige JSON(C): {{message}}", - "error.chain.configDirectoryTypo": - 'Map "{{dir}}" in {{path}} is niet geldig. Hernoem de map naar "{{suggestion}}" of verwijder deze. Dit is een veelvoorkomende typfout.', - "error.chain.configFrontmatterError": "Parsen van frontmatter mislukt in {{path}}:\n{{message}}", - "error.chain.configInvalid": "Configuratiebestand op {{path}} is ongeldig", - "error.chain.configInvalidWithMessage": "Configuratiebestand op {{path}} is ongeldig: {{message}}", - - "notification.permission.title": "Machtiging vereist", - "notification.permission.description": "{{sessionTitle}} in {{projectName}} heeft machtiging nodig", - "notification.question.title": "Vraag", - "notification.question.description": "{{sessionTitle}} in {{projectName}} heeft een vraag", - "notification.action.goToSession": "Ga naar sessie", - - "notification.session.responseReady.title": "Antwoord gereed", - "notification.session.error.title": "Sessiefout", - "notification.session.error.fallbackDescription": "Er is een fout opgetreden", - - "home.recentProjects": "Recente projecten", - "home.empty.title": "Geen recente projecten", - "home.empty.description": "Ga aan de slag door een lokaal project te openen", - - "session.tab.session": "Sessie", - "session.tab.review": "Review", - "session.tab.context": "Context", - "session.panel.reviewAndFiles": "Review en bestanden", - "session.review.filesChanged": "{{count}} Bestanden Gewijzigd", - "session.review.change.one": "Wijziging", - "session.review.change.other": "Wijzigingen", - "session.review.loadingChanges": "Wijzigingen laden...", - "session.review.empty": "Nog geen wijzigingen in deze sessie", - "session.review.noVcs": "Geen Git Version Control System gedetecteerd, wijzigingen niet weergegeven", - "session.review.noSnapshot": - "Snapshot-tracking is uitgeschakeld in configuratie, dus sessiewijzigingen zijn niet beschikbaar", - "session.review.noChanges": "Geen wijzigingen", - - "session.files.selectToOpen": "Selecteer een bestand om te openen", - "session.files.all": "Alle bestanden", - "session.files.empty": "Geen bestanden", - "session.files.binaryContent": "Binair bestand (inhoud kan niet worden weergegeven)", - - "session.messages.renderEarlier": "Eerdere berichten renderen", - "session.messages.loadingEarlier": "Eerdere berichten laden...", - "session.messages.loadEarlier": "Eerdere berichten laden", - "session.messages.loading": "Berichten laden...", - "session.messages.jumpToLatest": "Spring naar nieuwste", - - "session.context.addToContext": "Voeg {{selection}} toe aan context", - "session.todo.title": "To-do's", - "session.todo.collapse": "Inklappen", - "session.todo.expand": "Uitklappen", - - "session.new.title": "Bouw wat u wilt", - "session.new.worktree.main": "Hoofdbranch", - "session.new.worktree.mainWithBranch": "Hoofdbranch ({{branch}})", - "session.new.worktree.create": "Nieuwe worktree maken", - "session.new.lastModified": "Laatst gewijzigd", - - "session.header.search.placeholder": "Zoek {{project}}", - "session.header.searchFiles": "Zoek bestanden", - "session.header.openIn": "Openen in", - "session.header.open.action": "Open {{app}}", - "session.header.open.ariaLabel": "Openen in {{app}}", - "session.header.open.menu": "Open-opties", - "session.header.open.copyPath": "Pad kopiëren", - - "status.popover.trigger": "Status", - "status.popover.ariaLabel": "Serverconfiguraties", - "status.popover.tab.servers": "Servers", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "Plug-ins", - "status.popover.action.manageServers": "Servers beheren", - - "session.share.popover.title": "Op web publiceren", - "session.share.popover.description.shared": - "Deze sessie is openbaar op het web. Het is toegankelijk voor iedereen met de link.", - "session.share.popover.description.unshared": - "Sessie openbaar delen op het web. Het zal toegankelijk zijn voor iedereen met de link.", - "session.share.action.share": "Delen", - "session.share.action.publish": "Publiceren", - "session.share.action.publishing": "Publiceren...", - "session.share.action.unpublish": "Publicatie ongedaan maken", - "session.share.action.unpublishing": "Publicatie ongedaan maken...", - "session.share.action.view": "Bekijken", - "session.share.copy.copied": "Gekopieerd", - "session.share.copy.copyLink": "Link kopiëren", - - "lsp.tooltip.none": "Geen LSP-servers", - "lsp.label.connected": "{{count}} LSP", - - "prompt.loading": "Prompt laden...", - "terminal.loading": "Terminal laden...", - "terminal.title": "Terminal", - "terminal.title.numbered": "Terminal {{number}}", - "terminal.close": "Terminal sluiten", - "terminal.connectionLost.title": "Verbinding verbroken", - "terminal.connectionLost.description": - "De terminalverbinding is onderbroken. Dit kan gebeuren wanneer de server herstart.", - - "common.closeTab": "Tabblad sluiten", - "common.dismiss": "Negeren", - "common.moreCountSuffix": " (nog +{{count}})", - "common.requestFailed": "Verzoek mislukt", - "common.moreOptions": "Meer opties", - "common.learnMore": "Meer informatie", - "common.rename": "Hernoemen", - "common.reset": "Reset", - "common.archive": "Archiveren", - "common.delete": "Verwijderen", - "common.close": "Sluiten", - "common.edit": "Bewerken", - "common.loadMore": "Meer laden", - "common.key.esc": "ESC", - - "common.time.justNow": "Zojuist", - "common.time.minutesAgo.short": "{{count}}m geleden", - "common.time.hoursAgo.short": "{{count}}u geleden", - "common.time.daysAgo.short": "{{count}}d geleden", - - "sidebar.menu.toggle": "Menu in-/uitschakelen", - "sidebar.nav.projectsAndSessions": "Projecten en sessies", - "sidebar.settings": "Instellingen", - "sidebar.help": "Help", - "sidebar.workspaces.enable": "Werkruimtes inschakelen", - "sidebar.workspaces.disable": "Werkruimtes uitschakelen", - "sidebar.gettingStarted.title": "Aan de slag", - "sidebar.gettingStarted.line1": "Kilo bevat gratis modellen zodat u direct kunt beginnen.", - "sidebar.gettingStarted.line2": "Verbind een provider om modellen te gebruiken, incl. Claude, GPT, Gemini etc.", - "sidebar.project.recentSessions": "Recente sessies", - "sidebar.project.viewAllSessions": "Bekijk alle sessies", - "sidebar.project.clearNotifications": "Meldingen wissen", - - "app.name.desktop": "Kilo Desktop", - - "settings.section.desktop": "Desktop", - "settings.section.server": "Server", - "settings.tab.general": "Algemeen", - "settings.tab.shortcuts": "Sneltoetsen", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL-integratie", - "settings.desktop.wsl.description": "Draai de Kilo server binnen WSL op Windows.", - - "settings.general.section.appearance": "Uiterlijk", - "settings.general.section.notifications": "Systeemmeldingen", - "settings.general.section.updates": "Updates", - "settings.general.section.sounds": "Geluidseffecten", - "settings.general.section.feed": "Feed", - "settings.general.section.display": "Weergave", - - "settings.general.row.language.title": "Taal", - "settings.general.row.language.description": "Wijzig de weergavetaal voor Kilo", - "settings.general.row.appearance.title": "Uiterlijk", - "settings.general.row.appearance.description": "Pas aan hoe Kilo eruitziet op uw apparaat", - "settings.general.row.theme.title": "Thema", - "settings.general.row.theme.description": "Pas het thema van Kilo aan.", - "settings.general.row.font.title": "Lettertype", - "settings.general.row.font.description": "Pas het monospace-lettertype in codeblokken aan", - "settings.general.row.reasoningSummaries.title": "Redeneersamenvattingen weergeven", - "settings.general.row.reasoningSummaries.description": "Toon model-redeneersamenvattingen in de tijdlijn", - "settings.general.row.shellToolPartsExpanded.title": "Shell-toolonderdelen uitklappen", - "settings.general.row.shellToolPartsExpanded.description": - "Toon shell-toolonderdelen standaard uitgeklapt in de tijdlijn", - "settings.general.row.editToolPartsExpanded.title": "Bewerk-toolonderdelen uitklappen", - "settings.general.row.editToolPartsExpanded.description": - "Toon bewerk-, schrijf- en patch-toolonderdelen standaard uitgeklapt in de tijdlijn", - - "settings.general.row.wayland.title": "Gebruik native Wayland", - "settings.general.row.wayland.description": "Schakel X11-terugval uit op Wayland. Vereist een herstart.", - "settings.general.row.wayland.tooltip": - "Op Linux met monitoren met gemengde verversingssnelheden kan native Wayland stabieler zijn.", - - "settings.general.row.releaseNotes.title": "Release notes", - "settings.general.row.releaseNotes.description": "Toon Wat is nieuw pop-ups na updates", - - "settings.updates.row.startup.title": "Controleren op updates bij opstarten", - "settings.updates.row.startup.description": "Automatisch op updates controleren wanneer Kilo start", - "settings.updates.row.check.title": "Controleren op updates", - "settings.updates.row.check.description": "Handmatig op updates controleren en installeren indien beschikbaar", - "settings.updates.action.checkNow": "Nu controleren", - "settings.updates.action.checking": "Controleren...", - "settings.updates.toast.latest.title": "U bent up-to-date", - "settings.updates.toast.latest.description": "U draait de nieuwste versie van Kilo.", - "font.option.ibmPlexMono": "IBM Plex Mono", - "font.option.cascadiaCode": "Cascadia Code", - "font.option.firaCode": "Fira Code", - "font.option.hack": "Hack", - "font.option.inconsolata": "Inconsolata", - "font.option.intelOneMono": "Intel One Mono", - "font.option.iosevka": "Iosevka", - "font.option.jetbrainsMono": "JetBrains Mono", - "font.option.mesloLgs": "Meslo LGS", - "font.option.robotoMono": "Roboto Mono", - "font.option.sourceCodePro": "Source Code Pro", - "font.option.ubuntuMono": "Ubuntu Mono", - "font.option.geistMono": "Geist Mono", - "sound.option.none": "Geen", - "sound.option.alert01": "Melding 01", - "sound.option.alert02": "Melding 02", - "sound.option.alert03": "Melding 03", - "sound.option.alert04": "Melding 04", - "sound.option.alert05": "Melding 05", - "sound.option.alert06": "Melding 06", - "sound.option.alert07": "Melding 07", - "sound.option.alert08": "Melding 08", - "sound.option.alert09": "Melding 09", - "sound.option.alert10": "Melding 10", - "sound.option.bipbop01": "Bip-bop 01", - "sound.option.bipbop02": "Bip-bop 02", - "sound.option.bipbop03": "Bip-bop 03", - "sound.option.bipbop04": "Bip-bop 04", - "sound.option.bipbop05": "Bip-bop 05", - "sound.option.bipbop06": "Bip-bop 06", - "sound.option.bipbop07": "Bip-bop 07", - "sound.option.bipbop08": "Bip-bop 08", - "sound.option.bipbop09": "Bip-bop 09", - "sound.option.bipbop10": "Bip-bop 10", - "sound.option.staplebops01": "Staplebops 01", - "sound.option.staplebops02": "Staplebops 02", - "sound.option.staplebops03": "Staplebops 03", - "sound.option.staplebops04": "Staplebops 04", - "sound.option.staplebops05": "Staplebops 05", - "sound.option.staplebops06": "Staplebops 06", - "sound.option.staplebops07": "Staplebops 07", - "sound.option.nope01": "Nee 01", - "sound.option.nope02": "Nee 02", - "sound.option.nope03": "Nee 03", - "sound.option.nope04": "Nee 04", - "sound.option.nope05": "Nee 05", - "sound.option.nope06": "Nee 06", - "sound.option.nope07": "Nee 07", - "sound.option.nope08": "Nee 08", - "sound.option.nope09": "Nee 09", - "sound.option.nope10": "Nee 10", - "sound.option.nope11": "Nee 11", - "sound.option.nope12": "Nee 12", - "sound.option.yup01": "Ja 01", - "sound.option.yup02": "Ja 02", - "sound.option.yup03": "Ja 03", - "sound.option.yup04": "Ja 04", - "sound.option.yup05": "Ja 05", - "sound.option.yup06": "Ja 06", - - "settings.general.notifications.agent.title": "Agent", - "settings.general.notifications.agent.description": - "Toon systeemmelding wanneer de agent klaar is of aandacht nodig heeft", - "settings.general.notifications.permissions.title": "Machtigingen", - "settings.general.notifications.permissions.description": "Toon systeemmelding wanneer een machtiging vereist is", - "settings.general.notifications.errors.title": "Fouten", - "settings.general.notifications.errors.description": "Toon systeemmelding wanneer er een fout optreedt", - - "settings.general.sounds.agent.title": "Agent", - "settings.general.sounds.agent.description": "Speel geluid af wanneer de agent klaar is of aandacht nodig heeft", - "settings.general.sounds.permissions.title": "Machtigingen", - "settings.general.sounds.permissions.description": "Speel geluid af wanneer een machtiging vereist is", - "settings.general.sounds.errors.title": "Fouten", - "settings.general.sounds.errors.description": "Speel geluid af wanneer er een fout optreedt", - - "settings.shortcuts.title": "Sneltoetsen", - "settings.shortcuts.reset.button": "Terugzetten naar standaard", - "settings.shortcuts.reset.toast.title": "Sneltoetsen gereset", - "settings.shortcuts.reset.toast.description": "Sneltoetsen zijn teruggezet naar de standaardwaarden.", - "settings.shortcuts.conflict.title": "Sneltoets al in gebruik", - "settings.shortcuts.conflict.description": "{{keybind}} is al toegewezen aan {{titles}}.", - "settings.shortcuts.unassigned": "Niet toegewezen", - "settings.shortcuts.pressKeys": "Druk op toetsen", - "settings.shortcuts.search.placeholder": "Zoek sneltoetsen", - "settings.shortcuts.search.empty": "Geen sneltoetsen gevonden", - - "settings.shortcuts.group.general": "Algemeen", - "settings.shortcuts.group.session": "Sessie", - "settings.shortcuts.group.navigation": "Navigatie", - "settings.shortcuts.group.modelAndAgent": "Model en agent", - "settings.shortcuts.group.terminal": "Terminal", - "settings.shortcuts.group.prompt": "Prompt", - - "settings.providers.title": "Providers", - "settings.providers.description": "Provider-instellingen kunnen hier worden geconfigureerd.", - "settings.providers.section.connected": "Verbonden providers", - "settings.providers.connected.empty": "Geen verbonden providers", - "settings.providers.connected.environmentDescription": "Verbonden vanuit uw omgevingsvariabelen", - "settings.providers.section.popular": "Populaire providers", - "settings.providers.custom.description": "Voeg een OpenAI-compatibele provider toe via basis-URL.", - "settings.providers.tag.environment": "Omgeving", - "settings.providers.tag.config": "Config", - "settings.providers.tag.custom": "Aangepast", - "settings.providers.tag.other": "Overig", - "settings.models.title": "Modellen", - "settings.models.description": "Model-instellingen kunnen hier worden geconfigureerd.", - "settings.agents.title": "Agenten", - "settings.agents.description": "Agent-instellingen kunnen hier worden geconfigureerd.", - "settings.commands.title": "Opdrachten", - "settings.commands.description": "Opdracht-instellingen kunnen hier worden geconfigureerd.", - "settings.mcp.title": "MCP", - "settings.mcp.description": "MCP-instellingen kunnen hier worden geconfigureerd.", - - "settings.permissions.title": "Machtigingen", - "settings.permissions.description": "Bepaal welke tools de server standaard mag gebruiken.", - "settings.permissions.section.tools": "Tools", - "settings.permissions.toast.updateFailed.title": "Bijwerken van machtigingen mislukt", - - "settings.permissions.action.allow": "Toestaan", - "settings.permissions.action.ask": "Vragen", - "settings.permissions.action.deny": "Weigeren", - - "settings.permissions.tool.read.title": "Lezen (Read)", - "settings.permissions.tool.read.description": "Een bestand lezen (komt overeen met het bestandspad)", - "settings.permissions.tool.edit.title": "Bewerken (Edit)", - "settings.permissions.tool.edit.description": - "Bestanden wijzigen, inclusief bewerkingen, wegschrijven, patches en multi-edits", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "Bestanden matchen met glob-patronen", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "Bestandsinhoud doorzoeken met behulp van reguliere expressies", - "settings.permissions.tool.list.title": "Lijst (List)", - "settings.permissions.tool.list.description": "Bestanden in een map weergeven", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "Shell-opdrachten uitvoeren", - "settings.permissions.tool.task.title": "Taak (Task)", - "settings.permissions.tool.task.description": "Sub-agenten lanceren", - "settings.permissions.tool.skill.title": "Skill", - "settings.permissions.tool.skill.description": "Een skill laden op naam", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "Language server query's uitvoeren", - "settings.permissions.tool.todoread.title": "To-do lezen (Todo Read)", - "settings.permissions.tool.todoread.description": "De to-do lijst lezen", - "settings.permissions.tool.todowrite.title": "To-do schrijven (Todo Write)", - "settings.permissions.tool.todowrite.description": "De to-do lijst bijwerken", - "settings.permissions.tool.webfetch.title": "Web ophalen (Web Fetch)", - "settings.permissions.tool.webfetch.description": "Inhoud ophalen van een URL", - "settings.permissions.tool.websearch.title": "Web zoeken (Web Search)", - "settings.permissions.tool.websearch.description": "Het web doorzoeken", - "settings.permissions.tool.codesearch.title": "Code zoeken (Code Search)", - "settings.permissions.tool.codesearch.description": "Zoeken naar code op het web", - "settings.permissions.tool.external_directory.title": "Externe Map", - "settings.permissions.tool.external_directory.description": "Toegang tot bestanden buiten de projectmap", - "settings.permissions.tool.doom_loop.title": "Eindeloze Loop (Doom Loop)", - "settings.permissions.tool.doom_loop.description": "Gedetecteerde herhaalde tool-aanroepen met identieke invoer", - - "session.delete.failed.title": "Verwijderen van sessie mislukt", - "session.delete.title": "Sessie verwijderen", - "session.delete.confirm": 'Sessie "{{name}}" verwijderen?', - "session.delete.button": "Sessie verwijderen", - - "workspace.new": "Nieuwe werkruimte", - "workspace.type.local": "lokaal", - "workspace.type.sandbox": "sandbox", - "workspace.create.failed.title": "Maken van werkruimte mislukt", - "workspace.delete.failed.title": "Verwijderen van werkruimte mislukt", - "workspace.resetting.title": "Werkruimte resetten", - "workspace.resetting.description": "Dit kan een minuut duren.", - "workspace.reset.failed.title": "Resetten van werkruimte mislukt", - "workspace.reset.success.title": "Werkruimte gereset", - "workspace.reset.success.description": "Werkruimte komt nu overeen met de standaardbranch.", - "workspace.error.stillPreparing": "Werkruimte is nog aan het voorbereiden", - "workspace.status.checking": "Controleren op ongemergde wijzigingen...", - "workspace.status.error": "Kan git-status niet verifiëren.", - "workspace.status.clean": "Geen ongemergde wijzigingen gedetecteerd.", - "workspace.status.dirty": "Ongemergde wijzigingen gedetecteerd in deze werkruimte.", - "workspace.delete.title": "Werkruimte verwijderen", - "workspace.delete.confirm": 'Werkruimte "{{name}}" verwijderen?', - "workspace.delete.button": "Werkruimte verwijderen", - "workspace.reset.title": "Werkruimte resetten", - "workspace.reset.confirm": 'Werkruimte "{{name}}" resetten?', - "workspace.reset.button": "Werkruimte resetten", - "workspace.reset.archived.none": "Geen actieve sessies zullen worden gearchiveerd.", - "workspace.reset.archived.one": "1 sessie zal worden gearchiveerd.", - "workspace.reset.archived.many": "{{count}} sessies zullen worden gearchiveerd.", - "workspace.reset.note": "Dit zal de werkruimte resetten zodat deze overeenkomt met de standaardbranch.", -} diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts deleted file mode 100644 index 1551f956821..00000000000 --- a/packages/app/src/i18n/no.ts +++ /dev/null @@ -1,936 +0,0 @@ -import { dict as en } from "./en" -type Keys = keyof typeof en - -export const dict = { - "command.category.suggested": "Foreslått", - "command.category.view": "Visning", - "command.category.project": "Prosjekt", - "command.category.provider": "Leverandør", - "command.category.server": "Server", - "command.category.session": "Sesjon", - "command.category.theme": "Tema", - "command.category.language": "Språk", - "command.category.file": "Fil", - "command.category.context": "Kontekst", - "command.category.terminal": "Terminal", - "command.category.model": "Modell", - "command.category.mcp": "MCP", - "command.category.agent": "Agent", - "command.category.permissions": "Tillatelser", - "command.category.workspace": "Arbeidsområde", - "command.category.settings": "Innstillinger", - - "theme.scheme.system": "System", - "theme.scheme.light": "Lys", - "theme.scheme.dark": "Mørk", - - "command.sidebar.toggle": "Veksle sidepanel", - "command.project.open": "Åpne prosjekt", - "command.provider.connect": "Koble til leverandør", - "command.server.switch": "Bytt server", - "command.settings.open": "Åpne innstillinger", - "command.session.previous": "Forrige sesjon", - "command.session.next": "Neste sesjon", - "command.session.previous.unseen": "Forrige uleste økt", - "command.session.next.unseen": "Neste uleste økt", - "command.session.archive": "Arkiver sesjon", - - "command.palette": "Kommandopalett", - - "command.theme.cycle": "Bytt tema", - "command.theme.set": "Bruk tema: {{theme}}", - "command.theme.scheme.cycle": "Bytt fargevalg", - "command.theme.scheme.set": "Bruk fargevalg: {{scheme}}", - - "command.language.cycle": "Bytt språk", - "command.language.set": "Bruk språk: {{language}}", - - "command.session.new": "Ny sesjon", - "command.file.open": "Åpne fil", - "command.tab.close": "Lukk fane", - "command.context.addSelection": "Legg til markering i kontekst", - "command.context.addSelection.description": "Legg til valgte linjer fra gjeldende fil", - "command.input.focus": "Fokuser inndata", - "command.terminal.toggle": "Veksle terminal", - "command.fileTree.toggle": "Veksle filtre", - "command.review.toggle": "Veksle gjennomgang", - "command.terminal.new": "Ny terminal", - "command.terminal.new.description": "Opprett en ny terminalfane", - "command.steps.toggle": "Veksle trinn", - "command.steps.toggle.description": "Vis eller skjul trinn for gjeldende melding", - "command.message.previous": "Forrige melding", - "command.message.previous.description": "Gå til forrige brukermelding", - "command.message.next": "Neste melding", - "command.message.next.description": "Gå til neste brukermelding", - "command.model.choose": "Velg modell", - "command.model.choose.description": "Velg en annen modell", - "command.mcp.toggle": "Veksle MCP-er", - "command.mcp.toggle.description": "Veksle MCP-er", - "command.agent.cycle": "Bytt agent", - "command.agent.cycle.description": "Bytt til neste agent", - "command.agent.cycle.reverse": "Bytt agent bakover", - "command.agent.cycle.reverse.description": "Bytt til forrige agent", - "command.model.variant.cycle": "Bytt tenkeinnsats", - "command.model.variant.cycle.description": "Bytt til neste innsatsnivå", - "command.prompt.mode.shell": "Shell", - "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Aksepter tillatelser automatisk", - "command.permissions.autoaccept.disable": "Stopp automatisk akseptering av tillatelser", - "command.workspace.toggle": "Veksle arbeidsområder", - "command.workspace.toggle.description": "Enable or disable multiple workspaces in the sidebar", - "command.session.undo": "Angre", - "command.session.undo.description": "Angre siste melding", - "command.session.redo": "Gjør om", - "command.session.redo.description": "Gjør om siste angrede melding", - "command.session.compact": "Komprimer sesjon", - "command.session.compact.description": "Oppsummer sesjonen for å redusere kontekststørrelsen", - "command.session.fork": "Forgren fra melding", - "command.session.fork.description": "Opprett en ny sesjon fra en tidligere melding", - "command.session.share": "Del sesjon", - "command.session.share.description": "Del denne sesjonen og kopier URL-en til utklippstavlen", - "command.session.unshare": "Slutt å dele sesjon", - "command.session.unshare.description": "Slutt å dele denne sesjonen", - - "palette.search.placeholder": "Søk i filer, kommandoer og sesjoner", - "palette.empty": "Ingen resultater funnet", - "palette.group.commands": "Kommandoer", - "palette.group.files": "Filer", - - "dialog.provider.search.placeholder": "Søk etter leverandører", - "dialog.provider.empty": "Ingen leverandører funnet", - "dialog.provider.group.popular": "Populære", - "dialog.provider.group.other": "Andre", - "dialog.provider.tag.recommended": "Anbefalt", - "dialog.provider.opencode.note": "Utvalgte modeller inkludert Claude, GPT, Gemini og mer", - "dialog.provider.opencode.tagline": "Pålitelige, optimaliserte modeller", - "dialog.provider.opencodeGo.tagline": "Rimelig abonnement for alle", - "dialog.provider.anthropic.note": "Direkte tilgang til Claude-modeller, inkludert Pro og Max", - "dialog.provider.copilot.note": "AI-modeller for kodeassistanse via GitHub Copilot", - "dialog.provider.openai.note": "GPT-modeller for raske, dyktige generelle AI-oppgaver", - "dialog.provider.google.note": "Gemini-modeller for raske, strukturerte svar", - "dialog.provider.openrouter.note": "Tilgang til alle støttede modeller fra én leverandør", - "dialog.provider.vercel.note": "Enhetlig tilgang til AI-modeller med smart ruting", - - "dialog.model.select.title": "Velg modell", - "dialog.model.search.placeholder": "Søk etter modeller", - "dialog.model.empty": "Ingen modellresultater", - "dialog.model.manage": "Administrer modeller", - "dialog.model.manage.description": "Tilpass hvilke modeller som vises i modellvelgeren.", - "dialog.model.manage.provider.toggle": "Veksle alle {{provider}}-modeller", - - "dialog.model.unpaid.freeModels.title": "Gratis modeller levert av Kilo", - "dialog.model.unpaid.addMore.title": "Legg til flere modeller fra populære leverandører", - - "dialog.provider.viewAll": "Vis flere leverandører", - - "provider.connect.title": "Koble til {{provider}}", - "provider.connect.title.anthropicProMax": "Logg inn med Claude Pro/Max", - "provider.connect.selectMethod": "Velg innloggingsmetode for {{provider}}.", - "provider.connect.method.apiKey": "API-nøkkel", - "provider.connect.status.inProgress": "Autorisering pågår...", - "provider.connect.status.waiting": "Venter på autorisering...", - "provider.connect.status.failed": "Autorisering mislyktes: {{error}}", - "provider.connect.apiKey.description": - "Skriv inn din {{provider}} API-nøkkel for å koble til kontoen din og bruke {{provider}}-modeller i Kilo.", - "provider.connect.apiKey.label": "{{provider}} API-nøkkel", - "provider.connect.apiKey.placeholder": "API-nøkkel", - "provider.connect.apiKey.required": "API-nøkkel er påkrevd", - "provider.connect.opencodeZen.line1": - "OpenCode Zen gir deg tilgang til et utvalg av pålitelige optimaliserte modeller for kodeagenter.", - "provider.connect.opencodeZen.line2": - "Med én enkelt API-nøkkel får du tilgang til modeller som Claude, GPT, Gemini, GLM og flere.", - "provider.connect.opencodeZen.visit.prefix": "Besøk ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " for å hente API-nøkkelen din.", - "provider.connect.oauth.code.visit.prefix": "Besøk ", - "provider.connect.oauth.code.visit.link": "denne lenken", - "provider.connect.oauth.code.visit.suffix": - " for å hente autorisasjonskoden din for å koble til kontoen din og bruke {{provider}}-modeller i Kilo.", - "provider.connect.oauth.code.label": "{{method}} autorisasjonskode", - "provider.connect.oauth.code.placeholder": "Autorisasjonskode", - "provider.connect.oauth.code.required": "Autorisasjonskode er påkrevd", - "provider.connect.oauth.code.invalid": "Ugyldig autorisasjonskode", - "provider.connect.oauth.auto.visit.prefix": "Besøk ", - "provider.connect.oauth.auto.visit.link": "denne lenken", - "provider.connect.oauth.auto.visit.suffix": - " og skriv inn koden nedenfor for å koble til kontoen din og bruke {{provider}}-modeller i Kilo.", - "provider.connect.oauth.auto.confirmationCode": "Bekreftelseskode", - "provider.connect.toast.connected.title": "{{provider}} tilkoblet", - "provider.connect.toast.connected.description": "{{provider}}-modeller er nå tilgjengelige.", - - "provider.custom.title": "Egendefinert leverandør", - "provider.custom.description.prefix": "Konfigurer en OpenAI-kompatibel leverandør. Se ", - "provider.custom.description.link": "dokumentasjon for leverandørkonfigurasjon", - "provider.custom.description.suffix": ".", - "provider.custom.field.providerID.label": "Leverandør-ID", - "provider.custom.field.providerID.placeholder": "minleverandør", - "provider.custom.field.providerID.description": "Små bokstaver, tall, bindestreker eller understreker", - "provider.custom.field.name.label": "Visningsnavn", - "provider.custom.field.name.placeholder": "Min AI-leverandør", - "provider.custom.field.baseURL.label": "Base-URL", - "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", - "provider.custom.field.apiKey.label": "API-nøkkel", - "provider.custom.field.apiKey.placeholder": "API-nøkkel", - "provider.custom.field.apiKey.description": "Valgfritt. La stå tomt hvis du administrerer autentisering via headers.", - "provider.custom.models.label": "Modeller", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "modell-id", - "provider.custom.models.name.label": "Navn", - "provider.custom.models.name.placeholder": "Visningsnavn", - "provider.custom.models.remove": "Fjern modell", - "provider.custom.models.add": "Legg til modell", - "provider.custom.headers.label": "Headers (valgfritt)", - "provider.custom.headers.key.label": "Header", - "provider.custom.headers.key.placeholder": "Header-Navn", - "provider.custom.headers.value.label": "Verdi", - "provider.custom.headers.value.placeholder": "verdi", - "provider.custom.headers.remove": "Fjern header", - "provider.custom.headers.add": "Legg til header", - "provider.custom.error.providerID.required": "Leverandør-ID er påkrevd", - "provider.custom.error.providerID.format": "Bruk små bokstaver, tall, bindestreker eller understreker", - "provider.custom.error.providerID.exists": "Den leverandør-IDen finnes allerede", - "provider.custom.error.name.required": "Visningsnavn er påkrevd", - "provider.custom.error.baseURL.required": "Base-URL er påkrevd", - "provider.custom.error.baseURL.format": "Må starte med http:// eller https://", - "provider.custom.error.required": "Påkrevd", - "provider.custom.error.duplicate": "Duplikat", - - "provider.disconnect.toast.disconnected.title": "{{provider}} frakoblet", - "provider.disconnect.toast.disconnected.description": "Modeller fra {{provider}} er ikke lenger tilgjengelige.", - - "model.tag.free": "Gratis", - "model.tag.latest": "Nyeste", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "tekst", - "model.input.image": "bilde", - "model.input.audio": "lyd", - "model.input.video": "video", - "model.input.pdf": "pdf", - "model.tooltip.allows": "Tillater: {{inputs}}", - "model.tooltip.reasoning.allowed": "Tillater resonnering", - "model.tooltip.reasoning.none": "Ingen resonnering", - "model.tooltip.context": "Kontekstgrense {{limit}}", - - "common.search.placeholder": "Søk", - "common.goBack": "Gå tilbake", - "common.goForward": "Gå frem", - "common.loading": "Laster", - "common.loading.ellipsis": "...", - "common.cancel": "Avbryt", - "common.connect": "Koble til", - "common.disconnect": "Koble fra", - "common.continue": "Send inn", - "common.submit": "Send inn", - "common.save": "Lagre", - "common.saving": "Lagrer...", - "common.default": "Standard", - "common.attachment": "vedlegg", - - "prompt.placeholder.shell": "Skriv inn shell-kommando... {{example}}", - "prompt.placeholder.normal": 'Spør om hva som helst... "{{example}}"', - "prompt.placeholder.simple": "Spør om hva som helst...", - "prompt.placeholder.summarizeComments": "Oppsummer kommentarer…", - "prompt.placeholder.summarizeComment": "Oppsummer kommentar…", - "prompt.mode.shell": "Shell", - "prompt.mode.normal": "Prompt", - "prompt.mode.shell.exit": "ESC for å avslutte", - - "prompt.example.1": "Fiks en TODO i kodebasen", - "prompt.example.2": "Hva er teknologistabelen i dette prosjektet?", - "prompt.example.3": "Fiks ødelagte tester", - "prompt.example.4": "Forklar hvordan autentisering fungerer", - "prompt.example.5": "Finn og fiks sikkerhetssårbarheter", - "prompt.example.6": "Legg til enhetstester for brukerservicen", - "prompt.example.7": "Refaktorer denne funksjonen for bedre lesbarhet", - "prompt.example.8": "Hva betyr denne feilen?", - "prompt.example.9": "Hjelp meg med å feilsøke dette problemet", - "prompt.example.10": "Generer API-dokumentasjon", - "prompt.example.11": "Optimaliser databasespørringer", - "prompt.example.12": "Legg til inputvalidering", - "prompt.example.13": "Lag en ny komponent for...", - "prompt.example.14": "Hvordan deployer jeg dette prosjektet?", - "prompt.example.15": "Gjennomgå koden min for beste praksis", - "prompt.example.16": "Legg til feilhåndtering i denne funksjonen", - "prompt.example.17": "Forklar dette regex-mønsteret", - "prompt.example.18": "Konverter dette til TypeScript", - "prompt.example.19": "Legg til logging i hele kodebasen", - "prompt.example.20": "Hvilke avhengigheter er utdaterte?", - "prompt.example.21": "Hjelp meg med å skrive et migreringsskript", - "prompt.example.22": "Implementer caching for dette endepunktet", - "prompt.example.23": "Legg til paginering i denne listen", - "prompt.example.24": "Lag en CLI-kommando for...", - "prompt.example.25": "Hvordan fungerer miljøvariabler her?", - - "prompt.popover.emptyResults": "Ingen matchende resultater", - "prompt.popover.emptyCommands": "Ingen matchende kommandoer", - "prompt.dropzone.label": "Slipp bilder, PDF-er eller tekstfiler her", - "prompt.dropzone.file.label": "Slipp for å @nevne fil", - "prompt.slash.badge.custom": "egendefinert", - "prompt.slash.badge.skill": "skill", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "aktiv", - "prompt.context.includeActiveFile": "Inkluder aktiv fil", - "prompt.context.removeActiveFile": "Fjern aktiv fil fra kontekst", - "prompt.context.removeFile": "Fjern fil fra kontekst", - "prompt.action.attachFile": "Legg ved fil", - "prompt.attachment.remove": "Fjern vedlegg", - "prompt.action.send": "Send", - "prompt.action.stop": "Stopp", - - "prompt.toast.pasteUnsupported.title": "Ikke støttet vedlegg", - "prompt.toast.pasteUnsupported.description": "Kun bilder, PDF-er eller tekstfiler kan legges ved her.", - "prompt.toast.modelAgentRequired.title": "Velg en agent og modell", - "prompt.toast.modelAgentRequired.description": "Velg en agent og modell før du sender en forespørsel.", - "prompt.toast.worktreeCreateFailed.title": "Kunne ikke opprette worktree", - "prompt.toast.sessionCreateFailed.title": "Kunne ikke opprette sesjon", - "prompt.toast.shellSendFailed.title": "Kunne ikke sende shell-kommando", - "prompt.toast.commandSendFailed.title": "Kunne ikke sende kommando", - "prompt.toast.promptSendFailed.title": "Kunne ikke sende forespørsel", - "prompt.toast.promptSendFailed.description": "Kunne ikke hente økt", - - "dialog.mcp.title": "MCP-er", - "dialog.mcp.description": "{{enabled}} av {{total}} aktivert", - "dialog.mcp.empty": "Ingen MCP-er konfigurert", - - "dialog.lsp.empty": "LSP-er automatisk oppdaget fra filtyper", - "dialog.plugins.empty": "Plugins konfigurert i opencode.json", - - "mcp.status.connected": "tilkoblet", - "mcp.status.failed": "mislyktes", - "mcp.status.needs_auth": "trenger autentisering", - "mcp.status.disabled": "deaktivert", - - "dialog.fork.empty": "Ingen meldinger å forgrene fra", - - "dialog.directory.search.placeholder": "Søk etter mapper", - "dialog.directory.empty": "Ingen mapper funnet", - - "dialog.server.title": "Servere", - "dialog.server.description": "Bytt hvilken Kilo-server denne appen kobler til.", - "dialog.server.search.placeholder": "Søk etter servere", - "dialog.server.empty": "Ingen servere ennå", - "dialog.server.add.title": "Legg til en server", - "dialog.server.add.url": "Server-URL", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "Kunne ikke koble til server", - "dialog.server.add.checking": "Sjekker...", - "dialog.server.add.button": "Legg til server", - "dialog.server.add.name": "Servernavn (valgfritt)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "Brukernavn (valgfritt)", - "dialog.server.add.password": "Passord (valgfritt)", - "dialog.server.edit.title": "Rediger server", - "dialog.server.default.title": "Standardserver", - "dialog.server.default.description": - "Koble til denne serveren ved oppstart i stedet for å starte en lokal server. Krever omstart.", - "dialog.server.default.none": "Ingen server valgt", - "dialog.server.default.set": "Sett gjeldende server som standard", - "dialog.server.default.clear": "Tøm", - "dialog.server.action.remove": "Fjern server", - - "dialog.server.menu.edit": "Rediger", - "dialog.server.menu.default": "Sett som standard", - "dialog.server.menu.defaultRemove": "Fjern standard", - "dialog.server.menu.delete": "Slett", - "dialog.server.current": "Gjeldende server", - "dialog.server.status.default": "Standard", - - "dialog.project.edit.title": "Rediger prosjekt", - "dialog.project.edit.name": "Navn", - "dialog.project.edit.icon": "Ikon", - "dialog.project.edit.icon.alt": "Prosjektikon", - "dialog.project.edit.icon.hint": "Klikk eller dra et bilde", - "dialog.project.edit.icon.recommended": "Anbefalt: 128x128px", - "dialog.project.edit.color": "Farge", - "dialog.project.edit.color.select": "Velg fargen {{color}}", - "dialog.project.edit.worktree.startup": "Oppstartsskript for arbeidsområde", - "dialog.project.edit.worktree.startup.description": "Kjører etter at et nytt arbeidsområde (worktree) er opprettet.", - "dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install", - - "context.breakdown.title": "Kontekstfordeling", - "context.breakdown.note": 'Omtrentlig fordeling av input-tokens. "Annet" inkluderer verktøydefinisjoner og overhead.', - "context.breakdown.system": "System", - "context.breakdown.user": "Bruker", - "context.breakdown.assistant": "Assistent", - "context.breakdown.tool": "Verktøykall", - "context.breakdown.other": "Annet", - - "context.systemPrompt.title": "Systemprompt", - "context.rawMessages.title": "Rå meldinger", - - "context.stats.session": "Sesjon", - "context.stats.messages": "Meldinger", - "context.stats.provider": "Leverandør", - "context.stats.model": "Modell", - "context.stats.limit": "Kontekstgrense", - "context.stats.totalTokens": "Totalt antall tokens", - "context.stats.usage": "Forbruk", - "context.stats.inputTokens": "Input-tokens", - "context.stats.outputTokens": "Output-tokens", - "context.stats.reasoningTokens": "Resonnerings-tokens", - "context.stats.cacheTokens": "Cache-tokens (les/skriv)", - "context.stats.userMessages": "Brukermeldinger", - "context.stats.assistantMessages": "Assistentmeldinger", - "context.stats.totalCost": "Total kostnad", - "context.stats.sessionCreated": "Sesjon opprettet", - "context.stats.lastActivity": "Siste aktivitet", - - "context.usage.tokens": "Tokens", - "context.usage.usage": "Forbruk", - "context.usage.cost": "Kostnad", - "context.usage.clickToView": "Klikk for å se kontekst", - "context.usage.view": "Se kontekstforbruk", - - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - - "toast.language.title": "Språk", - "toast.language.description": "Byttet til {{language}}", - - "toast.theme.title": "Tema byttet", - "toast.scheme.title": "Fargevalg", - - "toast.workspace.enabled.title": "Arbeidsområder aktivert", - "toast.workspace.enabled.description": "Flere worktrees vises nå i sidefeltet", - "toast.workspace.disabled.title": "Arbeidsområder deaktivert", - "toast.workspace.disabled.description": "Kun hoved-worktree vises i sidefeltet", - - "toast.permissions.autoaccept.on.title": "Aksepterer tillatelser automatisk", - "toast.permissions.autoaccept.on.description": "Forespørsler om tillatelse vil bli godkjent automatisk", - "toast.permissions.autoaccept.off.title": "Stoppet automatisk akseptering av tillatelser", - "toast.permissions.autoaccept.off.description": "Forespørsler om tillatelse vil kreve godkjenning", - - "toast.model.none.title": "Ingen modell valgt", - "toast.model.none.description": "Koble til en leverandør for å oppsummere denne sesjonen", - - "toast.file.loadFailed.title": "Kunne ikke laste fil", - "toast.file.listFailed.title": "Kunne ikke liste filer", - - "toast.context.noLineSelection.title": "Ingen linjevalg", - "toast.context.noLineSelection.description": "Velg først et linjeområde i en filfane.", - - "toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til utklippstavlen", - "toast.session.share.success.title": "Sesjon delt", - "toast.session.share.success.description": "Delings-URL kopiert til utklippstavlen!", - "toast.session.share.failed.title": "Kunne ikke dele sesjon", - "toast.session.share.failed.description": "Det oppstod en feil under deling av sesjonen", - - "toast.session.unshare.success.title": "Deling av sesjon stoppet", - "toast.session.unshare.success.description": "Sesjonen deles ikke lenger!", - "toast.session.unshare.failed.title": "Kunne ikke stoppe deling av sesjon", - "toast.session.unshare.failed.description": "Det oppstod en feil da delingen av sesjonen skulle stoppes", - - "toast.session.listFailed.title": "Kunne ikke laste sesjoner for {{project}}", - - "toast.update.title": "Oppdatering tilgjengelig", - "toast.update.description": "En ny versjon av Kilo ({{version}}) er nå tilgjengelig for installasjon.", - "toast.update.action.installRestart": "Installer og start på nytt", - "toast.update.action.notYet": "Ikke nå", - - "error.page.title": "Noe gikk galt", - "error.page.description": "Det oppstod en feil under lasting av applikasjonen.", - "error.page.details.label": "Feildetaljer", - "error.page.action.restart": "Start på nytt", - "error.page.action.checking": "Sjekker...", - "error.page.action.checkUpdates": "Se etter oppdateringer", - "error.page.action.updateTo": "Oppdater til {{version}}", - "error.page.report.prefix": "Vennligst rapporter denne feilen til Kilo-teamet", - "error.page.report.discord": "på Discord", - "error.page.version": "Versjon: {{version}}", - - "error.dev.rootNotFound": - "Rotelement ikke funnet. Glemte du å legge det til i index.html? Eller kanskje id-attributten er feilstavet?", - - "error.globalSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?", - "directory.error.invalidUrl": "Invalid directory in URL.", - - "error.chain.unknown": "Ukjent feil", - "error.chain.causedBy": "Forårsaket av:", - "error.chain.apiError": "API-feil", - "error.chain.status": "Status: {{status}}", - "error.chain.retryable": "Kan prøves på nytt: {{retryable}}", - "error.chain.responseBody": "Responsinnhold:\n{{body}}", - "error.chain.didYouMean": "Mente du: {{suggestions}}", - "error.chain.modelNotFound": "Modell ikke funnet: {{provider}}/{{model}}", - "error.chain.checkConfig": "Sjekk leverandør-/modellnavnene i konfigurasjonen din (opencode.json)", - "error.chain.mcpFailed": 'MCP-server "{{name}}" mislyktes. Merk at Kilo ikke støtter MCP-autentisering ennå.', - "error.chain.providerAuthFailed": "Leverandørautentisering mislyktes ({{provider}}): {{message}}", - "error.chain.providerInitFailed": - 'Kunne ikke initialisere leverandør "{{provider}}". Sjekk legitimasjon og konfigurasjon.', - "error.chain.configJsonInvalid": "Konfigurasjonsfilen på {{path}} er ikke gyldig JSON(C)", - "error.chain.configJsonInvalidWithMessage": "Konfigurasjonsfilen på {{path}} er ikke gyldig JSON(C): {{message}}", - "error.chain.configDirectoryTypo": - 'Mappen "{{dir}}" i {{path}} er ikke gyldig. Gi mappen nytt navn til "{{suggestion}}" eller fjern den. Dette er en vanlig skrivefeil.', - "error.chain.configFrontmatterError": "Kunne ikke analysere frontmatter i {{path}}:\n{{message}}", - "error.chain.configInvalid": "Konfigurasjonsfilen på {{path}} er ugyldig", - "error.chain.configInvalidWithMessage": "Konfigurasjonsfilen på {{path}} er ugyldig: {{message}}", - - "notification.permission.title": "Tillatelse påkrevd", - "notification.permission.description": "{{sessionTitle}} i {{projectName}} trenger tillatelse", - "notification.question.title": "Spørsmål", - "notification.question.description": "{{sessionTitle}} i {{projectName}} har et spørsmål", - "notification.action.goToSession": "Gå til sesjon", - - "notification.session.responseReady.title": "Svar klart", - "notification.session.error.title": "Sesjonsfeil", - "notification.session.error.fallbackDescription": "Det oppstod en feil", - - "home.recentProjects": "Nylige prosjekter", - "home.empty.title": "Ingen nylige prosjekter", - "home.empty.description": "Kom i gang ved å åpne et lokalt prosjekt", - - "session.tab.session": "Sesjon", - "session.tab.review": "Gjennomgang", - "session.tab.context": "Kontekst", - "session.panel.reviewAndFiles": "Gjennomgang og filer", - "session.review.filesChanged": "{{count}} filer endret", - "session.review.change.one": "Endring", - "session.review.change.other": "Endringer", - "session.review.loadingChanges": "Laster endringer...", - "session.review.empty": "Ingen endringer i denne sesjonen ennå", - "session.review.noVcs": "Ingen Git-versjonskontrollsystem oppdaget, endringer vises ikke", - "session.review.noSnapshot": - "Snapshot-sporing er deaktivert i konfigurasjonen, så sesjonsendringer er ikke tilgjengelige", - "session.review.noChanges": "Ingen endringer", - - "session.files.selectToOpen": "Velg en fil å åpne", - "session.files.all": "Alle filer", - "session.files.empty": "Ingen filer", - "session.files.binaryContent": "Binær fil (innhold kan ikke vises)", - - "session.messages.renderEarlier": "Vis tidligere meldinger", - "session.messages.loadingEarlier": "Laster inn tidligere meldinger...", - "session.messages.loadEarlier": "Last inn tidligere meldinger", - "session.messages.loading": "Laster meldinger...", - "session.messages.jumpToLatest": "Hopp til nyeste", - - "session.context.addToContext": "Legg til {{selection}} i kontekst", - "session.todo.title": "Oppgaver", - "session.todo.collapse": "Skjul", - "session.todo.expand": "Utvid", - "session.followupDock.summary.one": "{{count}} melding i kø", - "session.followupDock.summary.other": "{{count}} meldinger i kø", - "session.followupDock.sendNow": "Send nå", - "session.followupDock.edit": "Rediger", - "session.followupDock.collapse": "Skjul meldinger i kø", - "session.followupDock.expand": "Utvid meldinger i kø", - "session.revertDock.summary.one": "{{count}} tilbakestilt melding", - "session.revertDock.summary.other": "{{count}} tilbakestilte meldinger", - "session.revertDock.collapse": "Skjul tilbakestilte meldinger", - "session.revertDock.expand": "Utvid tilbakestilte meldinger", - "session.revertDock.restore": "Gjenopprett melding", - - "session.new.title": "Bygg hva som helst", - "session.new.worktree.main": "Hovedgren", - "session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})", - "session.new.worktree.create": "Opprett nytt worktree", - "session.new.lastModified": "Sist endret", - - "session.header.search.placeholder": "Søk i {{project}}", - "session.header.searchFiles": "Søk etter filer", - "session.header.openIn": "Åpne i", - "session.header.open.action": "Åpne {{app}}", - "session.header.open.ariaLabel": "Åpne i {{app}}", - "session.header.open.menu": "Åpne alternativer", - "session.header.open.copyPath": "Kopier bane", - - "status.popover.trigger": "Status", - "status.popover.ariaLabel": "Serverkonfigurasjoner", - "status.popover.tab.servers": "Servere", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "Plugins", - "status.popover.action.manageServers": "Administrer servere", - - "session.share.popover.title": "Publiser på nett", - "session.share.popover.description.shared": - "Denne sesjonen er offentlig på nettet. Den er tilgjengelig for alle med lenken.", - "session.share.popover.description.unshared": - "Del sesjonen offentlig på nettet. Den vil være tilgjengelig for alle med lenken.", - "session.share.action.share": "Del", - "session.share.action.publish": "Publiser", - "session.share.action.publishing": "Publiserer...", - "session.share.action.unpublish": "Avpubliser", - "session.share.action.unpublishing": "Avpubliserer...", - "session.share.action.view": "Vis", - "session.share.copy.copied": "Kopiert", - "session.share.copy.copyLink": "Kopier lenke", - - "lsp.tooltip.none": "Ingen LSP-servere", - "lsp.label.connected": "{{count}} LSP", - - "prompt.loading": "Laster prompt...", - "terminal.loading": "Laster terminal...", - "terminal.title": "Terminal", - "terminal.title.numbered": "Terminal {{number}}", - "terminal.close": "Lukk terminal", - "terminal.connectionLost.title": "Tilkobling mistet", - "terminal.connectionLost.description": - "Terminalforbindelsen ble avbrutt. Dette kan skje når serveren starter på nytt.", - - "common.closeTab": "Lukk fane", - "common.dismiss": "Avvis", - "common.requestFailed": "Forespørsel mislyktes", - "common.moreOptions": "Flere alternativer", - "common.learnMore": "Lær mer", - "common.rename": "Gi nytt navn", - "common.reset": "Tilbakestill", - "common.archive": "Arkiver", - "common.delete": "Slett", - "common.close": "Lukk", - "common.edit": "Rediger", - "common.loadMore": "Last flere", - "common.key.esc": "ESC", - - "sidebar.menu.toggle": "Veksle meny", - "sidebar.nav.projectsAndSessions": "Prosjekter og sesjoner", - "sidebar.settings": "Innstillinger", - "sidebar.help": "Hjelp", - "sidebar.workspaces.enable": "Aktiver arbeidsområder", - "sidebar.workspaces.disable": "Deaktiver arbeidsområder", - "sidebar.gettingStarted.title": "Kom i gang", - "sidebar.gettingStarted.line1": "Kilo inkluderer gratis modeller så du kan starte umiddelbart.", - "sidebar.gettingStarted.line2": "Koble til en leverandør for å bruke modeller, inkl. Claude, GPT, Gemini osv.", - "sidebar.project.recentSessions": "Nylige sesjoner", - "sidebar.project.viewAllSessions": "Vis alle sesjoner", - "sidebar.project.clearNotifications": "Fjern varsler", - - "app.name.desktop": "Kilo Desktop", - - "settings.section.desktop": "Skrivebord", - "settings.section.server": "Server", - "settings.tab.general": "Generelt", - "settings.tab.shortcuts": "Snarveier", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL-integrasjon", - "settings.desktop.wsl.description": "Kjør Kilo-serveren i WSL på Windows.", - - "settings.general.section.appearance": "Utseende", - "settings.general.section.notifications": "Systemvarsler", - "settings.general.section.updates": "Oppdateringer", - "settings.general.section.sounds": "Lydeffekter", - "settings.general.section.feed": "Feed", - "settings.general.section.display": "Skjerm", - - "settings.general.row.language.title": "Språk", - "settings.general.row.language.description": "Endre visningsspråket for Kilo", - "settings.general.row.appearance.title": "Utseende", - "settings.general.row.appearance.description": "Tilpass hvordan Kilo ser ut på enheten din", - "settings.general.row.colorScheme.title": "Fargevalg", - "settings.general.row.colorScheme.description": "Velg om Kilo skal følge systemets, lyst eller mørkt tema", - "settings.general.row.theme.title": "Tema", - "settings.general.row.theme.description": "Tilpass hvordan Kilo er tematisert.", - "settings.general.row.font.title": "Kodefont", - "settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "UI-skrift", - "settings.general.row.uiFont.description": "Tilpass skrifttypen som brukes i hele grensesnittet", - "settings.general.row.followup.title": "Oppfølgingsadferd", - "settings.general.row.followup.description": "Velg om oppfølgingsspørsmål skal kjøres umiddelbart eller vente i kø", - "settings.general.row.followup.option.queue": "Kø", - "settings.general.row.followup.option.steer": "Styr", - "settings.general.row.reasoningSummaries.title": "Vis resonneringssammendrag", - "settings.general.row.reasoningSummaries.description": "Vis sammendrag av modellresonnering i tidslinjen", - "settings.general.row.shellToolPartsExpanded.title": "Utvid shell-verktøydeler", - "settings.general.row.shellToolPartsExpanded.description": "Vis shell-verktøydeler utvidet som standard i tidslinjen", - "settings.general.row.editToolPartsExpanded.title": "Utvid edit-verktøydeler", - "settings.general.row.editToolPartsExpanded.description": - "Vis edit-, write- og patch-verktøydeler utvidet som standard i tidslinjen", - "settings.general.row.showSessionProgressBar.title": "Vis fremdriftslinje for sesjonen", - "settings.general.row.showSessionProgressBar.description": - "Vis den animerte fremdriftslinjen øverst i sesjonen når agenten jobber", - "settings.general.row.wayland.title": "Bruk innebygd Wayland", - "settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Krever omstart.", - "settings.general.row.wayland.tooltip": - "På Linux med skjermer med blandet oppdateringsfrekvens kan innebygd Wayland være mer stabilt.", - - "settings.general.row.releaseNotes.title": "Utgivelsesnotater", - "settings.general.row.releaseNotes.description": 'Vis "Hva er nytt"-vinduer etter oppdateringer', - - "settings.updates.row.startup.title": "Se etter oppdateringer ved oppstart", - "settings.updates.row.startup.description": "Se automatisk etter oppdateringer når Kilo starter", - "settings.updates.row.check.title": "Se etter oppdateringer", - "settings.updates.row.check.description": "Se etter oppdateringer manuelt og installer hvis tilgjengelig", - "settings.updates.action.checkNow": "Sjekk nå", - "settings.updates.action.checking": "Sjekker...", - "settings.updates.toast.latest.title": "Du er oppdatert", - "settings.updates.toast.latest.description": "Du bruker den nyeste versjonen av Kilo.", - "sound.option.none": "Ingen", - "sound.option.alert01": "Varsel 01", - "sound.option.alert02": "Varsel 02", - "sound.option.alert03": "Varsel 03", - "sound.option.alert04": "Varsel 04", - "sound.option.alert05": "Varsel 05", - "sound.option.alert06": "Varsel 06", - "sound.option.alert07": "Varsel 07", - "sound.option.alert08": "Varsel 08", - "sound.option.alert09": "Varsel 09", - "sound.option.alert10": "Varsel 10", - "sound.option.bipbop01": "Bip-bop 01", - "sound.option.bipbop02": "Bip-bop 02", - "sound.option.bipbop03": "Bip-bop 03", - "sound.option.bipbop04": "Bip-bop 04", - "sound.option.bipbop05": "Bip-bop 05", - "sound.option.bipbop06": "Bip-bop 06", - "sound.option.bipbop07": "Bip-bop 07", - "sound.option.bipbop08": "Bip-bop 08", - "sound.option.bipbop09": "Bip-bop 09", - "sound.option.bipbop10": "Bip-bop 10", - "sound.option.staplebops01": "Staplebops 01", - "sound.option.staplebops02": "Staplebops 02", - "sound.option.staplebops03": "Staplebops 03", - "sound.option.staplebops04": "Staplebops 04", - "sound.option.staplebops05": "Staplebops 05", - "sound.option.staplebops06": "Staplebops 06", - "sound.option.staplebops07": "Staplebops 07", - "sound.option.nope01": "Nei 01", - "sound.option.nope02": "Nei 02", - "sound.option.nope03": "Nei 03", - "sound.option.nope04": "Nei 04", - "sound.option.nope05": "Nei 05", - "sound.option.nope06": "Nei 06", - "sound.option.nope07": "Nei 07", - "sound.option.nope08": "Nei 08", - "sound.option.nope09": "Nei 09", - "sound.option.nope10": "Nei 10", - "sound.option.nope11": "Nei 11", - "sound.option.nope12": "Nei 12", - "sound.option.yup01": "Ja 01", - "sound.option.yup02": "Ja 02", - "sound.option.yup03": "Ja 03", - "sound.option.yup04": "Ja 04", - "sound.option.yup05": "Ja 05", - "sound.option.yup06": "Ja 06", - - "settings.general.notifications.agent.title": "Agent", - "settings.general.notifications.agent.description": - "Vis systemvarsel når agenten er ferdig eller trenger oppmerksomhet", - "settings.general.notifications.permissions.title": "Tillatelser", - "settings.general.notifications.permissions.description": "Vis systemvarsel når en tillatelse er påkrevd", - "settings.general.notifications.errors.title": "Feil", - "settings.general.notifications.errors.description": "Vis systemvarsel når det oppstår en feil", - - "settings.general.sounds.agent.title": "Agent", - "settings.general.sounds.agent.description": "Spill av lyd når agenten er ferdig eller trenger oppmerksomhet", - "settings.general.sounds.permissions.title": "Tillatelser", - "settings.general.sounds.permissions.description": "Spill av lyd når en tillatelse er påkrevd", - "settings.general.sounds.errors.title": "Feil", - "settings.general.sounds.errors.description": "Spill av lyd når det oppstår en feil", - - "settings.shortcuts.title": "Tastatursnarveier", - "settings.shortcuts.reset.button": "Tilbakestill til standard", - "settings.shortcuts.reset.toast.title": "Snarveier tilbakestilt", - "settings.shortcuts.reset.toast.description": "Tastatursnarveier er tilbakestilt til standard.", - "settings.shortcuts.conflict.title": "Snarvei allerede i bruk", - "settings.shortcuts.conflict.description": "{{keybind}} er allerede tilordnet til {{titles}}.", - "settings.shortcuts.unassigned": "Ikke tilordnet", - "settings.shortcuts.pressKeys": "Trykk taster", - "settings.shortcuts.search.placeholder": "Søk etter snarveier", - "settings.shortcuts.search.empty": "Ingen snarveier funnet", - - "settings.shortcuts.group.general": "Generelt", - "settings.shortcuts.group.session": "Sesjon", - "settings.shortcuts.group.navigation": "Navigasjon", - "settings.shortcuts.group.modelAndAgent": "Modell og agent", - "settings.shortcuts.group.terminal": "Terminal", - "settings.shortcuts.group.prompt": "Prompt", - - "settings.providers.title": "Leverandører", - "settings.providers.description": "Leverandørinnstillinger vil kunne konfigureres her.", - "settings.providers.section.connected": "Tilkoblede leverandører", - "settings.providers.connected.empty": "Ingen tilkoblede leverandører", - "settings.providers.section.popular": "Populære leverandører", - "settings.providers.tag.environment": "Miljø", - "settings.providers.tag.config": "Konfigurasjon", - "settings.providers.tag.custom": "Tilpasset", - "settings.providers.tag.other": "Annet", - "settings.models.title": "Modeller", - "settings.models.description": "Modellinnstillinger vil kunne konfigureres her.", - "settings.agents.title": "Agenter", - "settings.agents.description": "Agentinnstillinger vil kunne konfigureres her.", - "settings.commands.title": "Kommandoer", - "settings.commands.description": "Kommandoinnstillinger vil kunne konfigureres her.", - "settings.mcp.title": "MCP", - "settings.mcp.description": "MCP-innstillinger vil kunne konfigureres her.", - - "settings.permissions.title": "Tillatelser", - "settings.permissions.description": "Kontroller hvilke verktøy serveren kan bruke som standard.", - "settings.permissions.section.tools": "Verktøy", - "settings.permissions.toast.updateFailed.title": "Kunne ikke oppdatere tillatelser", - - "settings.permissions.action.allow": "Tillat", - "settings.permissions.action.ask": "Spør", - "settings.permissions.action.deny": "Avslå", - - "settings.permissions.tool.read.title": "Les", - "settings.permissions.tool.read.description": "Lesing av en fil (matcher filbanen)", - "settings.permissions.tool.edit.title": "Rediger", - "settings.permissions.tool.edit.description": - "Endre filer, inkludert redigeringer, skriving, patcher og multi-redigeringer", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "Match filer ved hjelp av glob-mønstre", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "Søk i filinnhold ved hjelp av regulære uttrykk", - "settings.permissions.tool.list.title": "Liste", - "settings.permissions.tool.list.description": "List filer i en mappe", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "Kjør shell-kommandoer", - "settings.permissions.tool.task.title": "Oppgave", - "settings.permissions.tool.task.description": "Start underagenter", - "settings.permissions.tool.skill.title": "Ferdighet", - "settings.permissions.tool.skill.description": "Last en ferdighet etter navn", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "Kjør språkserverforespørsler", - "settings.permissions.tool.todowrite.title": "Skriv gjøremål", - "settings.permissions.tool.todowrite.description": "Oppdater gjøremålslisten", - "settings.permissions.tool.webfetch.title": "Webhenting", - "settings.permissions.tool.webfetch.description": "Hent innhold fra en URL", - "settings.permissions.tool.websearch.title": "Websøk", - "settings.permissions.tool.websearch.description": "Søk på nettet", - "settings.permissions.tool.codesearch.title": "Kodesøk", - "settings.permissions.tool.codesearch.description": "Søk etter kode på nettet", - "settings.permissions.tool.external_directory.title": "Ekstern mappe", - "settings.permissions.tool.external_directory.description": "Få tilgang til filer utenfor prosjektmappen", - "settings.permissions.tool.doom_loop.title": "Doom Loop", - "settings.permissions.tool.doom_loop.description": "Oppdager gjentatte verktøykall med identisk input", - - "session.delete.failed.title": "Kunne ikke slette sesjon", - "session.delete.title": "Slett sesjon", - "session.delete.confirm": 'Slette sesjonen "{{name}}"?', - "session.delete.button": "Slett sesjon", - - "workspace.new": "Nytt arbeidsområde", - "workspace.type.local": "lokal", - "workspace.type.sandbox": "sandkasse", - "workspace.create.failed.title": "Kunne ikke opprette arbeidsområde", - "workspace.delete.failed.title": "Kunne ikke slette arbeidsområde", - "workspace.resetting.title": "Tilbakestiller arbeidsområde", - "workspace.resetting.description": "Dette kan ta et minutt.", - "workspace.reset.failed.title": "Kunne ikke tilbakestille arbeidsområde", - "workspace.reset.success.title": "Arbeidsområde tilbakestilt", - "workspace.reset.success.description": "Arbeidsområdet samsvarer nå med standardgrenen.", - "workspace.error.stillPreparing": "Arbeidsområdet klargjøres fortsatt", - "workspace.status.checking": "Sjekker for ikke-sammenslåtte endringer...", - "workspace.status.error": "Kunne ikke bekrefte git-status.", - "workspace.status.clean": "Ingen ikke-sammenslåtte endringer oppdaget.", - "workspace.status.dirty": "Ikke-sammenslåtte endringer oppdaget i dette arbeidsområdet.", - "workspace.delete.title": "Slett arbeidsområde", - "workspace.delete.confirm": 'Slette arbeidsområdet "{{name}}"?', - "workspace.delete.button": "Slett arbeidsområde", - "workspace.reset.title": "Tilbakestill arbeidsområde", - "workspace.reset.confirm": 'Tilbakestille arbeidsområdet "{{name}}"?', - "workspace.reset.button": "Tilbakestill arbeidsområde", - "workspace.reset.archived.none": "Ingen aktive sesjoner vil bli arkivert.", - "workspace.reset.archived.one": "1 sesjon vil bli arkivert.", - "workspace.reset.archived.many": "{{count}} sesjoner vil bli arkivert.", - "workspace.reset.note": "Dette vil tilbakestille arbeidsområdet til å samsvare med standardgrenen.", - "common.open": "Åpne", - "dialog.releaseNotes.action.getStarted": "Kom i gang", - "dialog.releaseNotes.action.next": "Neste", - "dialog.releaseNotes.action.hideFuture": "Ikke vis disse igjen", - "dialog.releaseNotes.media.alt": "Forhåndsvisning av utgivelse", - "toast.project.reloadFailed.title": "Kunne ikke laste inn {{project}} på nytt", - "error.server.invalidConfiguration": "Ugyldig konfigurasjon", - "common.moreCountSuffix": " (+{{count}} mer)", - "common.time.justNow": "Akkurat nå", - "common.time.minutesAgo.short": "{{count}} m siden", - "common.time.hoursAgo.short": "{{count}} t siden", - "common.time.daysAgo.short": "{{count}} d siden", - "settings.providers.connected.environmentDescription": "Koblet til fra miljøvariablene dine", - "settings.providers.custom.description": "Legg til en OpenAI-kompatibel leverandør via basis-URL.", - - "app.server.unreachable": "Kunne ikke nå {{server}}", - "app.server.retrying": "Prøver på nytt automatisk...", - "app.server.otherServers": "Andre servere", - "dialog.server.add.usernamePlaceholder": "brukernavn", - "dialog.server.add.passwordPlaceholder": "passord", - "server.row.noUsername": "inget brukernavn", - "session.review.noVcs.createGit.title": "Opprett et Git-depot", - "session.review.noVcs.createGit.description": "Spor, gjennomgå og angre endringer i dette prosjektet", - "session.review.noVcs.createGit.actionLoading": "Oppretter Git-depot...", - "session.review.noVcs.createGit.action": "Opprett Git-depot", - "session.todo.progress": "{{done}} av {{total}} oppgaver fullført", - "session.question.progress": "{{current}} av {{total}} spørsmål", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "Filutforsker", - "session.header.open.fileManager": "Filbehandler", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "Terminal", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "Utviklingsytelsesdiagnostikk", - "debugBar.na": "i/t", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": - "Siste fullførte ruteovergang som berører en sesjonsside, målt fra ruterstart til første opptegning etter at den har roet seg.", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "Rullende bilder per sekund over de siste 5 sekundene.", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "Verste bildetid over de siste 5 sekundene.", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "Bilder over 32ms i de siste 5 sekundene.", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "Blokkert tid og antall lange oppgaver i de siste 5 sekundene. Maks oppgave: {{max}}.", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "Verste observerte inndataforsinkelse i de siste 5 sekundene.", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": - "Omtrentlig interaksjonsvarighet over de siste 5 sekundene. Dette er INP-lignende, ikke den offisielle Web Vitals INP.", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "Kumulativ layoutforskyvning for gjeldende app-levetid.", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "Brukt JS-heap vs heap-grense. Kun Chromium.", - "debugBar.mem.tip": "Brukt JS-heap vs heap-grense. {{used}} av {{limit}}.", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "Mellomrom", - "common.key.backspace": "Backspace", - "common.key.enter": "Enter", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "ukjent", - "error.page.circular": "[Sirkulær]", - "error.globalSDK.noServerAvailable": "Ingen server tilgjengelig", - "error.globalSDK.serverNotAvailable": "Server ikke tilgjengelig", - "error.childStore.persistedCacheCreateFailed": "Kunne ikke opprette vedvarende hurtigbuffer", - "error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke opprette vedvarende prosjektmetadata", - "error.childStore.persistedProjectIconCreateFailed": "Kunne ikke opprette vedvarende prosjektikon", - "error.childStore.storeCreateFailed": "Kunne ikke opprette lager", - "terminal.connectionLost.abnormalClose": "WebSocket lukket unormalt: {{code}}", -} satisfies Partial> diff --git a/packages/app/src/i18n/parity.test.ts b/packages/app/src/i18n/parity.test.ts deleted file mode 100644 index c06a55ab171..00000000000 --- a/packages/app/src/i18n/parity.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { dict as en } from "./en" -import { dict as ar } from "./ar" -import { dict as br } from "./br" -import { dict as bs } from "./bs" -import { dict as da } from "./da" -import { dict as de } from "./de" -import { dict as es } from "./es" -import { dict as fr } from "./fr" -import { dict as ja } from "./ja" -import { dict as ko } from "./ko" -import { dict as no } from "./no" -import { dict as pl } from "./pl" -import { dict as ru } from "./ru" -import { dict as th } from "./th" -import { dict as zh } from "./zh" -import { dict as zht } from "./zht" -import { dict as tr } from "./tr" - -const locales = [ar, br, bs, da, de, es, fr, ja, ko, no, pl, ru, th, tr, zh, zht] -const keys = ["command.session.previous.unseen", "command.session.next.unseen"] as const - -describe("i18n parity", () => { - test("non-English locales translate targeted unseen session keys", () => { - for (const locale of locales) { - for (const key of keys) { - expect(locale[key]).toBeDefined() - expect(locale[key]).not.toBe(en[key]) - } - } - }) -}) diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts deleted file mode 100644 index 49e7e12d992..00000000000 --- a/packages/app/src/i18n/pl.ts +++ /dev/null @@ -1,857 +0,0 @@ -export const dict = { - "command.category.suggested": "Sugerowane", - "command.category.view": "Widok", - "command.category.project": "Projekt", - "command.category.provider": "Dostawca", - "command.category.server": "Serwer", - "command.category.session": "Sesja", - "command.category.theme": "Motyw", - "command.category.language": "Język", - "command.category.file": "Plik", - "command.category.context": "Kontekst", - "command.category.terminal": "Terminal", - "command.category.model": "Model", - "command.category.mcp": "MCP", - "command.category.agent": "Agent", - "command.category.permissions": "Uprawnienia", - "command.category.workspace": "Przestrzeń robocza", - "command.category.settings": "Ustawienia", - "theme.scheme.system": "Systemowy", - "theme.scheme.light": "Jasny", - "theme.scheme.dark": "Ciemny", - "command.sidebar.toggle": "Przełącz pasek boczny", - "command.project.open": "Otwórz projekt", - "command.provider.connect": "Połącz dostawcę", - "command.server.switch": "Przełącz serwer", - "command.settings.open": "Otwórz ustawienia", - "command.session.previous": "Poprzednia sesja", - "command.session.next": "Następna sesja", - "command.session.previous.unseen": "Poprzednia nieprzeczytana sesja", - "command.session.next.unseen": "Następna nieprzeczytana sesja", - "command.session.archive": "Zarchiwizuj sesję", - "command.palette": "Paleta poleceń", - "command.theme.cycle": "Przełącz motyw", - "command.theme.set": "Użyj motywu: {{theme}}", - "command.theme.scheme.cycle": "Przełącz schemat kolorów", - "command.theme.scheme.set": "Użyj schematu kolorów: {{scheme}}", - "command.language.cycle": "Przełącz język", - "command.language.set": "Użyj języka: {{language}}", - "command.session.new": "Nowa sesja", - "command.file.open": "Otwórz plik", - "command.tab.close": "Zamknij kartę", - "command.context.addSelection": "Dodaj zaznaczenie do kontekstu", - "command.context.addSelection.description": "Dodaj zaznaczone linie z bieżącego pliku", - "command.input.focus": "Fokus na pole wejściowe", - "command.terminal.toggle": "Przełącz terminal", - "command.fileTree.toggle": "Przełącz drzewo plików", - "command.review.toggle": "Przełącz przegląd", - "command.terminal.new": "Nowy terminal", - "command.terminal.new.description": "Utwórz nową kartę terminala", - "command.steps.toggle": "Przełącz kroki", - "command.steps.toggle.description": "Pokaż lub ukryj kroki dla bieżącej wiadomości", - "command.message.previous": "Poprzednia wiadomość", - "command.message.previous.description": "Przejdź do poprzedniej wiadomości użytkownika", - "command.message.next": "Następna wiadomość", - "command.message.next.description": "Przejdź do następnej wiadomości użytkownika", - "command.model.choose": "Wybierz model", - "command.model.choose.description": "Wybierz inny model", - "command.mcp.toggle": "Przełącz MCP", - "command.mcp.toggle.description": "Przełącz MCP", - "command.agent.cycle": "Przełącz agenta", - "command.agent.cycle.description": "Przełącz na następnego agenta", - "command.agent.cycle.reverse": "Przełącz agenta wstecz", - "command.agent.cycle.reverse.description": "Przełącz na poprzedniego agenta", - "command.model.variant.cycle": "Przełącz wysiłek myślowy", - "command.model.variant.cycle.description": "Przełącz na następny poziom wysiłku", - "command.prompt.mode.shell": "Terminal", - "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "Automatycznie akceptuj uprawnienia", - "command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie uprawnień", - "command.workspace.toggle": "Przełącz przestrzenie robocze", - "command.workspace.toggle.description": "Włącz lub wyłącz wiele przestrzeni roboczych na pasku bocznym", - "command.session.undo": "Cofnij", - "command.session.undo.description": "Cofnij ostatnią wiadomość", - "command.session.redo": "Ponów", - "command.session.redo.description": "Ponów ostatnią cofniętą wiadomość", - "command.session.compact": "Kompaktuj sesję", - "command.session.compact.description": "Podsumuj sesję, aby zmniejszyć rozmiar kontekstu", - "command.session.fork": "Rozwidlij od wiadomości", - "command.session.fork.description": "Utwórz nową sesję od poprzedniej wiadomości", - "command.session.share": "Udostępnij sesję", - "command.session.share.description": "Udostępnij tę sesję i skopiuj URL do schowka", - "command.session.unshare": "Przestań udostępniać sesję", - "command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji", - "palette.search.placeholder": "Szukaj plików, poleceń i sesji", - "palette.empty": "Brak wyników", - "palette.group.commands": "Polecenia", - "palette.group.files": "Pliki", - "dialog.provider.search.placeholder": "Szukaj dostawców", - "dialog.provider.empty": "Nie znaleziono dostawców", - "dialog.provider.group.popular": "Popularne", - "dialog.provider.group.other": "Inne", - "dialog.provider.tag.recommended": "Zalecane", - "dialog.provider.opencode.note": "Wyselekcjonowane modele, w tym Claude, GPT, Gemini i inne", - "dialog.provider.opencode.tagline": "Niezawodne, zoptymalizowane modele", - "dialog.provider.opencodeGo.tagline": "Tania subskrypcja dla każdego", - "dialog.provider.anthropic.note": "Bezpośredni dostęp do modeli Claude, w tym Pro i Max", - "dialog.provider.copilot.note": "Modele AI do pomocy w kodowaniu przez GitHub Copilot", - "dialog.provider.openai.note": "Modele GPT do szybkich i wszechstronnych zadań AI", - "dialog.provider.google.note": "Modele Gemini do szybkich i ustrukturyzowanych odpowiedzi", - "dialog.provider.openrouter.note": "Dostęp do wszystkich obsługiwanych modeli od jednego dostawcy", - "dialog.provider.vercel.note": "Ujednolicony dostęp do modeli AI z inteligentnym routingiem", - "dialog.model.select.title": "Wybierz model", - "dialog.model.search.placeholder": "Szukaj modeli", - "dialog.model.empty": "Brak wyników modelu", - "dialog.model.manage": "Zarządzaj modelami", - "dialog.model.manage.description": "Dostosuj, które modele pojawiają się w wyborze modelu.", - "dialog.model.manage.provider.toggle": "Przełącz wszystkie modele {{provider}}", - "dialog.model.unpaid.freeModels.title": "Darmowe modele dostarczane przez Kilo", - "dialog.model.unpaid.addMore.title": "Dodaj więcej modeli od popularnych dostawców", - "dialog.provider.viewAll": "Zobacz więcej dostawców", - "provider.connect.title": "Połącz {{provider}}", - "provider.connect.title.anthropicProMax": "Zaloguj się z Claude Pro/Max", - "provider.connect.selectMethod": "Wybierz metodę logowania dla {{provider}}.", - "provider.connect.method.apiKey": "Klucz API", - "provider.connect.status.inProgress": "Autoryzacja w toku...", - "provider.connect.status.waiting": "Oczekiwanie na autoryzację...", - "provider.connect.status.failed": "Autoryzacja nie powiodła się: {{error}}", - "provider.connect.apiKey.description": - "Wprowadź swój klucz API {{provider}}, aby połączyć konto i używać modeli {{provider}} w Kilo.", - "provider.connect.apiKey.label": "Klucz API {{provider}}", - "provider.connect.apiKey.placeholder": "Klucz API", - "provider.connect.apiKey.required": "Klucz API jest wymagany", - "provider.connect.opencodeZen.line1": - "OpenCode Zen daje dostęp do wybranego zestawu niezawodnych, zoptymalizowanych modeli dla agentów kodujących.", - "provider.connect.opencodeZen.line2": - "Z jednym kluczem API uzyskasz dostęp do modeli takich jak Claude, GPT, Gemini, GLM i więcej.", - "provider.connect.opencodeZen.visit.prefix": "Odwiedź ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": ", aby odebrać swój klucz API.", - "provider.connect.oauth.code.visit.prefix": "Odwiedź ", - "provider.connect.oauth.code.visit.link": "ten link", - "provider.connect.oauth.code.visit.suffix": - ", aby odebrać kod autoryzacyjny, połączyć konto i używać modeli {{provider}} w Kilo.", - "provider.connect.oauth.code.label": "Kod autoryzacyjny {{method}}", - "provider.connect.oauth.code.placeholder": "Kod autoryzacyjny", - "provider.connect.oauth.code.required": "Kod autoryzacyjny jest wymagany", - "provider.connect.oauth.code.invalid": "Nieprawidłowy kod autoryzacyjny", - "provider.connect.oauth.auto.visit.prefix": "Odwiedź ", - "provider.connect.oauth.auto.visit.link": "ten link", - "provider.connect.oauth.auto.visit.suffix": - " i wprowadź poniższy kod, aby połączyć konto i używać modeli {{provider}} w Kilo.", - "provider.connect.oauth.auto.confirmationCode": "Kod potwierdzający", - "provider.connect.toast.connected.title": "Połączono {{provider}}", - "provider.connect.toast.connected.description": "Modele {{provider}} są teraz dostępne do użycia.", - "provider.custom.title": "Dostawca niestandardowy", - "provider.custom.description.prefix": "Skonfiguruj dostawcę zgodnego z OpenAI. Zobacz ", - "provider.custom.description.link": "dokumentację konfiguracji dostawcy", - "provider.custom.description.suffix": ".", - "provider.custom.field.providerID.label": "ID dostawcy", - "provider.custom.field.providerID.placeholder": "mojdostawca", - "provider.custom.field.providerID.description": "Małe litery, cyfry, łączniki lub podkreślenia", - "provider.custom.field.name.label": "Nazwa wyświetlana", - "provider.custom.field.name.placeholder": "Mój Dostawca AI", - "provider.custom.field.baseURL.label": "Bazowy URL", - "provider.custom.field.baseURL.placeholder": "https://api.mojdostawca.com/v1", - "provider.custom.field.apiKey.label": "Klucz API", - "provider.custom.field.apiKey.placeholder": "Klucz API", - "provider.custom.field.apiKey.description": - "Opcjonalne. Pozostaw puste, jeśli zarządzasz autoryzacją przez nagłówki.", - "provider.custom.models.label": "Modele", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "model-id", - "provider.custom.models.name.label": "Nazwa", - "provider.custom.models.name.placeholder": "Nazwa wyświetlana", - "provider.custom.models.remove": "Usuń model", - "provider.custom.models.add": "Dodaj model", - "provider.custom.headers.label": "Nagłówki (opcjonalne)", - "provider.custom.headers.key.label": "Nagłówek", - "provider.custom.headers.key.placeholder": "Nazwa-Naglowka", - "provider.custom.headers.value.label": "Wartość", - "provider.custom.headers.value.placeholder": "wartość", - "provider.custom.headers.remove": "Usuń nagłówek", - "provider.custom.headers.add": "Dodaj nagłówek", - "provider.custom.error.providerID.required": "ID dostawcy jest wymagane", - "provider.custom.error.providerID.format": "Używaj małych liter, cyfr, łączników lub podkreśleń", - "provider.custom.error.providerID.exists": "To ID dostawcy już istnieje", - "provider.custom.error.name.required": "Nazwa wyświetlana jest wymagana", - "provider.custom.error.baseURL.required": "Bazowy URL jest wymagany", - "provider.custom.error.baseURL.format": "Musi zaczynać się od http:// lub https://", - "provider.custom.error.required": "Wymagane", - "provider.custom.error.duplicate": "Duplikat", - "provider.disconnect.toast.disconnected.title": "Rozłączono {{provider}}", - "provider.disconnect.toast.disconnected.description": "Modele {{provider}} nie są już dostępne.", - "model.tag.free": "Darmowy", - "model.tag.latest": "Najnowszy", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "tekst", - "model.input.image": "obraz", - "model.input.audio": "audio", - "model.input.video": "wideo", - "model.input.pdf": "pdf", - "model.tooltip.allows": "Obsługuje: {{inputs}}", - "model.tooltip.reasoning.allowed": "Obsługuje wnioskowanie", - "model.tooltip.reasoning.none": "Brak wnioskowania", - "model.tooltip.context": "Limit kontekstu {{limit}}", - "common.search.placeholder": "Szukaj", - "common.goBack": "Wstecz", - "common.goForward": "Dalej", - "common.loading": "Ładowanie", - "common.loading.ellipsis": "...", - "common.cancel": "Anuluj", - "common.connect": "Połącz", - "common.disconnect": "Rozłącz", - "common.continue": "Prześlij", - "common.submit": "Prześlij", - "common.save": "Zapisz", - "common.saving": "Zapisywanie...", - "common.default": "Domyślny", - "common.attachment": "załącznik", - "prompt.placeholder.shell": "Wpisz polecenie terminala... {{example}}", - "prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"', - "prompt.placeholder.simple": "Zapytaj o cokolwiek...", - "prompt.placeholder.summarizeComments": "Podsumuj komentarze…", - "prompt.placeholder.summarizeComment": "Podsumuj komentarz…", - "prompt.mode.shell": "Terminal", - "prompt.mode.normal": "Prompt", - "prompt.mode.shell.exit": "esc aby wyjść", - "prompt.example.1": "Napraw TODO w bazie kodu", - "prompt.example.2": "Jaki jest stos technologiczny tego projektu?", - "prompt.example.3": "Napraw zepsute testy", - "prompt.example.4": "Wyjaśnij jak działa uwierzytelnianie", - "prompt.example.5": "Znajdź i napraw luki w zabezpieczeniach", - "prompt.example.6": "Dodaj testy jednostkowe dla serwisu użytkownika", - "prompt.example.7": "Zrefaktoryzuj tę funkcję, aby była bardziej czytelna", - "prompt.example.8": "Co oznacza ten błąd?", - "prompt.example.9": "Pomóż mi zdebugować ten problem", - "prompt.example.10": "Wygeneruj dokumentację API", - "prompt.example.11": "Zoptymalizuj zapytania do bazy danych", - "prompt.example.12": "Dodaj walidację danych wejściowych", - "prompt.example.13": "Utwórz nowy komponent dla...", - "prompt.example.14": "Jak wdrożyć ten projekt?", - "prompt.example.15": "Sprawdź mój kod pod kątem najlepszych praktyk", - "prompt.example.16": "Dodaj obsługę błędów do tej funkcję", - "prompt.example.17": "Wyjaśnij ten wzorzec regex", - "prompt.example.18": "Przekonwertuj to na TypeScript", - "prompt.example.19": "Dodaj logowanie w całej bazie kodu", - "prompt.example.20": "Które zależności są przestarzałe?", - "prompt.example.21": "Pomóż mi napisać skrypt migracyjny", - "prompt.example.22": "Zaimplementuj cachowanie dla tego punktu końcowego", - "prompt.example.23": "Dodaj stronicowanie do tej listy", - "prompt.example.24": "Utwórz polecenie CLI dla...", - "prompt.example.25": "Jak działają tutaj zmienne środowiskowe?", - "prompt.popover.emptyResults": "Brak pasujących wyników", - "prompt.popover.emptyCommands": "Brak pasujących poleceń", - "prompt.dropzone.label": "Upuść tutaj obrazy, pliki PDF lub pliki tekstowe", - "prompt.dropzone.file.label": "Upuść, aby @wspomnieć plik", - "prompt.slash.badge.custom": "własne", - "prompt.slash.badge.skill": "skill", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "aktywny", - "prompt.context.includeActiveFile": "Dołącz aktywny plik", - "prompt.context.removeActiveFile": "Usuń aktywny plik z kontekstu", - "prompt.context.removeFile": "Usuń plik z kontekstu", - "prompt.action.attachFile": "Załącz plik", - "prompt.attachment.remove": "Usuń załącznik", - "prompt.action.send": "Wyślij", - "prompt.action.stop": "Zatrzymaj", - "prompt.toast.pasteUnsupported.title": "Nieobsługiwany załącznik", - "prompt.toast.pasteUnsupported.description": "Można tutaj załączać tylko obrazy, pliki PDF lub pliki tekstowe.", - "prompt.toast.modelAgentRequired.title": "Wybierz agenta i model", - "prompt.toast.modelAgentRequired.description": "Wybierz agenta i model przed wysłaniem zapytania.", - "prompt.toast.worktreeCreateFailed.title": "Nie udało się utworzyć drzewa roboczego", - "prompt.toast.sessionCreateFailed.title": "Nie udało się utworzyć sesji", - "prompt.toast.shellSendFailed.title": "Nie udało się wysłać polecenia powłoki", - "prompt.toast.commandSendFailed.title": "Nie udało się wysłać polecenia", - "prompt.toast.promptSendFailed.title": "Nie udało się wysłać zapytania", - "prompt.toast.promptSendFailed.description": "Nie udało się pobrać sesji", - "dialog.mcp.title": "MCP", - "dialog.mcp.description": "{{enabled}} z {{total}} włączone", - "dialog.mcp.empty": "Brak skonfigurowanych MCP", - "dialog.lsp.empty": "LSP wykryte automatycznie na podstawie typów plików", - "dialog.plugins.empty": "Wtyczki skonfigurowane w opencode.json", - "mcp.status.connected": "połączono", - "mcp.status.failed": "niepowodzenie", - "mcp.status.needs_auth": "wymaga autoryzacji", - "mcp.status.disabled": "wyłączone", - "dialog.fork.empty": "Brak wiadomości do rozwidlenia", - "dialog.directory.search.placeholder": "Szukaj folderów", - "dialog.directory.empty": "Nie znaleziono folderów", - "dialog.server.title": "Serwery", - "dialog.server.description": "Przełącz serwer Kilo, z którym łączy się ta aplikacja.", - "dialog.server.search.placeholder": "Szukaj serwerów", - "dialog.server.empty": "Brak serwerów", - "dialog.server.add.title": "Dodaj serwer", - "dialog.server.add.url": "URL serwera", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "Nie można połączyć się z serwerem", - "dialog.server.add.checking": "Sprawdzanie...", - "dialog.server.add.button": "Dodaj serwer", - "dialog.server.add.name": "Nazwa serwera (opcjonalnie)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "Nazwa użytkownika (opcjonalnie)", - "dialog.server.add.password": "Hasło (opcjonalnie)", - "dialog.server.edit.title": "Edytuj serwer", - "dialog.server.default.title": "Domyślny serwer", - "dialog.server.default.description": - "Połącz z tym serwerem przy uruchomieniu aplikacji zamiast uruchamiać lokalny serwer. Wymaga restartu.", - "dialog.server.default.none": "Nie wybrano serwera", - "dialog.server.default.set": "Ustaw bieżący serwer jako domyślny", - "dialog.server.default.clear": "Wyczyść", - "dialog.server.action.remove": "Usuń serwer", - "dialog.server.menu.edit": "Edytuj", - "dialog.server.menu.default": "Ustaw jako domyślny", - "dialog.server.menu.defaultRemove": "Usuń domyślny", - "dialog.server.menu.delete": "Usuń", - "dialog.server.current": "Obecny serwer", - "dialog.server.status.default": "Domyślny", - "dialog.project.edit.title": "Edytuj projekt", - "dialog.project.edit.name": "Nazwa", - "dialog.project.edit.icon": "Ikona", - "dialog.project.edit.icon.alt": "Ikona projektu", - "dialog.project.edit.icon.hint": "Kliknij lub przeciągnij obraz", - "dialog.project.edit.icon.recommended": "Zalecane: 128x128px", - "dialog.project.edit.color": "Kolor", - "dialog.project.edit.color.select": "Wybierz kolor {{color}}", - "dialog.project.edit.worktree.startup": "Skrypt uruchamiania przestrzeni roboczej", - "dialog.project.edit.worktree.startup.description": "Runs after creating a new workspace (worktree).", - "dialog.project.edit.worktree.startup.placeholder": "np. bun install", - "context.breakdown.title": "Podział kontekstu", - "context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.', - "context.breakdown.system": "System", - "context.breakdown.user": "Użytkownik", - "context.breakdown.assistant": "Asystent", - "context.breakdown.tool": "Wywołania narzędzi", - "context.breakdown.other": "Inne", - "context.systemPrompt.title": "Prompt systemowy", - "context.rawMessages.title": "Surowe wiadomości", - "context.stats.session": "Sesja", - "context.stats.messages": "Wiadomości", - "context.stats.provider": "Dostawca", - "context.stats.model": "Model", - "context.stats.limit": "Limit kontekstu", - "context.stats.totalTokens": "Całkowita liczba tokenów", - "context.stats.usage": "Użycie", - "context.stats.inputTokens": "Tokeny wejściowe", - "context.stats.outputTokens": "Tokeny wyjściowe", - "context.stats.reasoningTokens": "Tokeny wnioskowania", - "context.stats.cacheTokens": "Tokeny pamięci podręcznej (odczyt/zapis)", - "context.stats.userMessages": "Wiadomości użytkownika", - "context.stats.assistantMessages": "Wiadomości asystenta", - "context.stats.totalCost": "Całkowity koszt", - "context.stats.sessionCreated": "Utworzono sesję", - "context.stats.lastActivity": "Ostatnia aktywność", - "context.usage.tokens": "Tokeny", - "context.usage.usage": "Użycie", - "context.usage.cost": "Koszt", - "context.usage.clickToView": "Kliknij, aby zobaczyć kontekst", - "context.usage.view": "Pokaż użycie kontekstu", - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - "toast.language.title": "Język", - "toast.language.description": "Przełączono na {{language}}", - "toast.theme.title": "Przełączono motyw", - "toast.scheme.title": "Schemat kolorów", - "toast.workspace.enabled.title": "Przestrzenie robocze włączone", - "toast.workspace.enabled.description": "Kilka worktree jest teraz wyświetlanych na pasku bocznym", - "toast.workspace.disabled.title": "Przestrzenie robocze wyłączone", - "toast.workspace.disabled.description": "Tylko główny worktree jest wyświetlany na pasku bocznym", - "toast.permissions.autoaccept.on.title": "Automatyczne akceptowanie uprawnień", - "toast.permissions.autoaccept.on.description": "Żądania uprawnień będą automatycznie zatwierdzane", - "toast.permissions.autoaccept.off.title": "Zatrzymano automatyczne akceptowanie uprawnień", - "toast.permissions.autoaccept.off.description": "Żądania uprawnień będą wymagały zatwierdzenia", - "toast.model.none.title": "Nie wybrano modelu", - "toast.model.none.description": "Połącz dostawcę, aby podsumować tę sesję", - "toast.file.loadFailed.title": "Nie udało się załadować pliku", - "toast.file.listFailed.title": "Nie udało się wyświetlić listy plików", - "toast.context.noLineSelection.title": "Brak zaznaczenia linii", - "toast.context.noLineSelection.description": "Najpierw wybierz zakres linii w zakładce pliku.", - "toast.session.share.copyFailed.title": "Nie udało się skopiować URL do schowka", - "toast.session.share.success.title": "Sesja udostępniona", - "toast.session.share.success.description": "Link udostępniania skopiowany do schowka!", - "toast.session.share.failed.title": "Nie udało się udostępnić sesji", - "toast.session.share.failed.description": "Wystąpił błąd podczas udostępniania sesji", - "toast.session.unshare.success.title": "Zatrzymano udostępnianie sesji", - "toast.session.unshare.success.description": "Udostępnianie sesji zostało pomyślnie zatrzymane!", - "toast.session.unshare.failed.title": "Nie udało się zatrzymać udostępniania sesji", - "toast.session.unshare.failed.description": "Wystąpił błąd podczas zatrzymywania udostępniania sesji", - "toast.session.listFailed.title": "Nie udało się załadować sesji dla {{project}}", - "toast.update.title": "Dostępna aktualizacja", - "toast.update.description": "Nowa wersja Kilo ({{version}}) jest teraz dostępna do instalacji.", - "toast.update.action.installRestart": "Zainstaluj i zrestartuj", - "toast.update.action.notYet": "Jeszcze nie", - "error.page.title": "Coś poszło nie tak", - "error.page.description": "Wystąpił błąd podczas ładowania aplikacji.", - "error.page.details.label": "Szczegóły błędu", - "error.page.action.restart": "Restartuj", - "error.page.action.checking": "Sprawdzanie...", - "error.page.action.checkUpdates": "Sprawdź aktualizacje", - "error.page.action.updateTo": "Zaktualizuj do {{version}}", - "error.page.report.prefix": "Proszę zgłosić ten błąd do zespołu Kilo", - "error.page.report.discord": "na Discordzie", - "error.page.version": "Wersja: {{version}}", - "error.dev.rootNotFound": - "Nie znaleziono elementu głównego. Czy zapomniałeś dodać go do swojego index.html? A może atrybut id został błędnie wpisany?", - "error.globalSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?", - "directory.error.invalidUrl": "Nieprawidłowy katalog w URL.", - "error.chain.unknown": "Nieznany błąd", - "error.chain.causedBy": "Spowodowany przez:", - "error.chain.apiError": "Błąd API", - "error.chain.status": "Status: {{status}}", - "error.chain.retryable": "Można ponowić: {{retryable}}", - "error.chain.responseBody": "Treść odpowiedzi:\n{{body}}", - "error.chain.didYouMean": "Czy miałeś na myśli: {{suggestions}}", - "error.chain.modelNotFound": "Model nie znaleziony: {{provider}}/{{model}}", - "error.chain.checkConfig": "Sprawdź swoją konfigurację (opencode.json) nazwy dostawców/modeli", - "error.chain.mcpFailed": 'MCP server "{{name}}" failed. Note, Kilo does not support MCP authentication yet.', - "error.chain.providerAuthFailed": "Uwierzytelnianie dostawcy nie powiodło się ({{provider}}): {{message}}", - "error.chain.providerInitFailed": - 'Nie udało się zainicjować dostawcy "{{provider}}". Sprawdź poświadczenia i konfigurację.', - "error.chain.configJsonInvalid": "Plik konfiguracyjny w {{path}} nie jest poprawnym JSON(C)", - "error.chain.configJsonInvalidWithMessage": "Plik konfiguracyjny w {{path}} nie jest poprawnym JSON(C): {{message}}", - "error.chain.configDirectoryTypo": - 'Katalog "{{dir}}" w {{path}} jest nieprawidłowy. Zmień nazwę katalogu na "{{suggestion}}" lub usuń go. To częsta literówka.', - "error.chain.configFrontmatterError": "Nie udało się przetworzyć frontmatter w {{path}}:\n{{message}}", - "error.chain.configInvalid": "Plik konfiguracyjny w {{path}} jest nieprawidłowy", - "error.chain.configInvalidWithMessage": "Plik konfiguracyjny w {{path}} jest nieprawidłowy: {{message}}", - "notification.permission.title": "Wymagane uprawnienie", - "notification.permission.description": "{{sessionTitle}} w {{projectName}} potrzebuje uprawnienia", - "notification.question.title": "Pytanie", - "notification.question.description": "{{sessionTitle}} w {{projectName}} ma pytanie", - "notification.action.goToSession": "Przejdź do sesji", - "notification.session.responseReady.title": "Odpowiedź gotowa", - "notification.session.error.title": "Błąd sesji", - "notification.session.error.fallbackDescription": "Wystąpił błąd", - "home.recentProjects": "Ostatnie projekty", - "home.empty.title": "Brak ostatnich projektów", - "home.empty.description": "Zacznij od otwarcia lokalnego projektu", - "session.tab.session": "Sesja", - "session.tab.review": "Przegląd", - "session.tab.context": "Kontekst", - "session.panel.reviewAndFiles": "Przegląd i pliki", - "session.review.filesChanged": "Zmieniono {{count}} plików", - "session.review.change.one": "Zmiana", - "session.review.change.other": "Zmiany", - "session.review.loadingChanges": "Ładowanie zmian...", - "session.review.empty": "Brak zmian w tej sesji", - "session.review.noVcs": "Nie wykryto systemu kontroli wersji Git, zmiany nie są wyświetlane", - "session.review.noSnapshot": "Śledzenie migawek jest wyłączone w konfiguracji, więc zmiany w sesji są niedostępne", - "session.review.noChanges": "Brak zmian", - "session.files.selectToOpen": "Wybierz plik do otwarcia", - "session.files.all": "Wszystkie pliki", - "session.files.empty": "Brak plików", - "session.files.binaryContent": "Plik binarny (zawartość nie może być wyświetlona)", - "session.messages.renderEarlier": "Renderuj wcześniejsze wiadomości", - "session.messages.loadingEarlier": "Ładowanie wcześniejszych wiadomości...", - "session.messages.loadEarlier": "Załaduj wcześniejsze wiadomości", - "session.messages.loading": "Ładowanie wiadomości...", - "session.messages.jumpToLatest": "Przejdź do najnowszych", - "session.context.addToContext": "Dodaj {{selection}} do kontekstu", - "session.todo.title": "Zadania", - "session.todo.collapse": "Zwiń", - "session.todo.expand": "Rozwiń", - "session.followupDock.summary.one": "{{count}} wiadomość w kolejce", - "session.followupDock.summary.other": "{{count}} wiadomości w kolejce", - "session.followupDock.sendNow": "Wyślij teraz", - "session.followupDock.edit": "Edytuj", - "session.followupDock.collapse": "Zwiń wiadomości w kolejce", - "session.followupDock.expand": "Rozwiń wiadomości w kolejce", - "session.revertDock.summary.one": "{{count}} cofnięta wiadomość", - "session.revertDock.summary.other": "{{count}} cofnięte wiadomości", - "session.revertDock.collapse": "Zwiń cofnięte wiadomości", - "session.revertDock.expand": "Rozwiń cofnięte wiadomości", - "session.revertDock.restore": "Przywróć wiadomość", - "session.new.title": "Zbuduj cokolwiek", - "session.new.worktree.main": "Główna gałąź", - "session.new.worktree.mainWithBranch": "Główna gałąź ({{branch}})", - "session.new.worktree.create": "Utwórz nowe drzewo robocze", - "session.new.lastModified": "Ostatnio zmodyfikowano", - "session.header.search.placeholder": "Szukaj {{project}}", - "session.header.searchFiles": "Szukaj plików", - "session.header.openIn": "Otwórz w", - "session.header.open.action": "Otwórz {{app}}", - "session.header.open.ariaLabel": "Otwórz w {{app}}", - "session.header.open.menu": "Opcje otwierania", - "session.header.open.copyPath": "Kopiuj ścieżkę", - "status.popover.trigger": "Status", - "status.popover.ariaLabel": "Konfiguracje serwerów", - "status.popover.tab.servers": "Serwery", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "Wtyczki", - "status.popover.action.manageServers": "Zarządzaj serwerami", - "session.share.popover.title": "Opublikuj w sieci", - "session.share.popover.description.shared": - "Ta sesja jest publiczna w sieci. Jest dostępna dla każdego, kto posiada link.", - "session.share.popover.description.unshared": - "Udostępnij sesję publicznie w sieci. Będzie dostępna dla każdego, kto posiada link.", - "session.share.action.share": "Udostępnij", - "session.share.action.publish": "Opublikuj", - "session.share.action.publishing": "Publikowanie...", - "session.share.action.unpublish": "Cofnij publikację", - "session.share.action.unpublishing": "Cofanie publikacji...", - "session.share.action.view": "Widok", - "session.share.copy.copied": "Skopiowano", - "session.share.copy.copyLink": "Kopiuj link", - "lsp.tooltip.none": "Brak serwerów LSP", - "lsp.label.connected": "{{count}} LSP", - "prompt.loading": "Ładowanie promptu...", - "terminal.loading": "Ładowanie terminala...", - "terminal.title": "Terminal", - "terminal.title.numbered": "Terminal {{number}}", - "terminal.close": "Zamknij terminal", - "terminal.connectionLost.title": "Utracono połączenie", - "terminal.connectionLost.description": - "Połączenie z terminalem zostało przerwane. Może się to zdarzyć przy restarcie serwera.", - "common.closeTab": "Zamknij kartę", - "common.dismiss": "Odrzuć", - "common.requestFailed": "Żądanie nie powiodło się", - "common.moreOptions": "Więcej opcji", - "common.learnMore": "Dowiedz się więcej", - "common.rename": "Zmień nazwę", - "common.reset": "Resetuj", - "common.archive": "Archiwizuj", - "common.delete": "Usuń", - "common.close": "Zamknij", - "common.edit": "Edytuj", - "common.loadMore": "Załaduj więcej", - "common.key.esc": "ESC", - "sidebar.menu.toggle": "Przełącz menu", - "sidebar.nav.projectsAndSessions": "Projekty i sesje", - "sidebar.settings": "Ustawienia", - "sidebar.help": "Pomoc", - "sidebar.workspaces.enable": "Włącz przestrzenie robocze", - "sidebar.workspaces.disable": "Wyłącz przestrzenie robocze", - "sidebar.gettingStarted.title": "Pierwsze kroki", - "sidebar.gettingStarted.line1": "Kilo zawiera darmowe modele, więc możesz zacząć od razu.", - "sidebar.gettingStarted.line2": "Połącz dowolnego dostawcę, aby używać modeli, w tym Claude, GPT, Gemini itp.", - "sidebar.project.recentSessions": "Ostatnie sesje", - "sidebar.project.viewAllSessions": "Zobacz wszystkie sesje", - "sidebar.project.clearNotifications": "Wyczyść powiadomienia", - "app.name.desktop": "Kilo Desktop", - "settings.section.desktop": "Pulpit", - "settings.section.server": "Serwer", - "settings.tab.general": "Ogólne", - "settings.tab.shortcuts": "Skróty", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL integration", - "settings.desktop.wsl.description": "Run the Kilo server inside WSL on Windows.", - "settings.general.section.appearance": "Wygląd", - "settings.general.section.notifications": "Powiadomienia systemowe", - "settings.general.section.updates": "Aktualizacje", - "settings.general.section.sounds": "Efekty dźwiękowe", - "settings.general.section.feed": "Kanał", - "settings.general.section.display": "Ekran", - "settings.general.row.language.title": "Język", - "settings.general.row.language.description": "Zmień język wyświetlania dla Kilo", - "settings.general.row.appearance.title": "Wygląd", - "settings.general.row.appearance.description": "Dostosuj wygląd Kilo na swoim urządzeniu", - "settings.general.row.colorScheme.title": "Schemat kolorów", - "settings.general.row.colorScheme.description": - "Wybierz, czy Kilo ma używać motywu systemowego, jasnego czy ciemnego", - "settings.general.row.theme.title": "Motyw", - "settings.general.row.theme.description": "Dostosuj motyw Kilo.", - "settings.general.row.font.title": "Czcionka kodu", - "settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "Czcionka interfejsu", - "settings.general.row.uiFont.description": "Dostosuj czcionkę używaną w całym interfejsie", - "settings.general.row.followup.title": "Zachowanie kontynuacji", - "settings.general.row.followup.description": "Wybierz, czy kontynuacja ma być natychmiastowa, czy czekać w kolejce", - "settings.general.row.followup.option.queue": "Kolejka", - "settings.general.row.followup.option.steer": "Sterowanie", - "settings.general.row.reasoningSummaries.title": "Pokaż podsumowania wnioskowania", - "settings.general.row.reasoningSummaries.description": "Wyświetlaj podsumowania wnioskowania modelu na osi czasu", - "settings.general.row.shellToolPartsExpanded.title": "Rozwijaj elementy narzędzia shell", - "settings.general.row.shellToolPartsExpanded.description": - "Domyślnie pokazuj rozwinięte elementy narzędzia shell na osi czasu", - "settings.general.row.editToolPartsExpanded.title": "Rozwijaj elementy narzędzia edit", - "settings.general.row.editToolPartsExpanded.description": - "Domyślnie pokazuj rozwinięte elementy narzędzi edit, write i patch na osi czasu", - "settings.general.row.showSessionProgressBar.title": "Pokazuj pasek postępu sesji", - "settings.general.row.showSessionProgressBar.description": - "Wyświetlaj animowany pasek postępu u góry sesji, gdy agent pracuje", - "settings.general.row.wayland.title": "Użyj natywnego Wayland", - "settings.general.row.wayland.description": "Wyłącz fallback X11 na Wayland. Wymaga restartu.", - "settings.general.row.wayland.tooltip": - "Na Linuxie z monitorami o różnym odświeżaniu, natywny Wayland może być bardziej stabilny.", - "settings.general.row.releaseNotes.title": "Informacje o wydaniu", - "settings.general.row.releaseNotes.description": 'Pokazuj wyskakujące okna "Co nowego" po aktualizacjach', - "settings.updates.row.startup.title": "Sprawdzaj aktualizacje przy uruchomieniu", - "settings.updates.row.startup.description": "Automatycznie sprawdzaj aktualizacje podczas uruchamiania Kilo", - "settings.updates.row.check.title": "Sprawdź aktualizacje", - "settings.updates.row.check.description": "Ręcznie sprawdź aktualizacje i zainstaluj, jeśli są dostępne", - "settings.updates.action.checkNow": "Sprawdź teraz", - "settings.updates.action.checking": "Sprawdzanie...", - "settings.updates.toast.latest.title": "Masz najnowszą wersję", - "settings.updates.toast.latest.description": "Korzystasz z najnowszej wersji Kilo.", - "sound.option.none": "Brak", - "sound.option.alert01": "Alert 01", - "sound.option.alert02": "Alert 02", - "sound.option.alert03": "Alert 03", - "sound.option.alert04": "Alert 04", - "sound.option.alert05": "Alert 05", - "sound.option.alert06": "Alert 06", - "sound.option.alert07": "Alert 07", - "sound.option.alert08": "Alert 08", - "sound.option.alert09": "Alert 09", - "sound.option.alert10": "Alert 10", - "sound.option.bipbop01": "Bip-bop 01", - "sound.option.bipbop02": "Bip-bop 02", - "sound.option.bipbop03": "Bip-bop 03", - "sound.option.bipbop04": "Bip-bop 04", - "sound.option.bipbop05": "Bip-bop 05", - "sound.option.bipbop06": "Bip-bop 06", - "sound.option.bipbop07": "Bip-bop 07", - "sound.option.bipbop08": "Bip-bop 08", - "sound.option.bipbop09": "Bip-bop 09", - "sound.option.bipbop10": "Bip-bop 10", - "sound.option.staplebops01": "Staplebops 01", - "sound.option.staplebops02": "Staplebops 02", - "sound.option.staplebops03": "Staplebops 03", - "sound.option.staplebops04": "Staplebops 04", - "sound.option.staplebops05": "Staplebops 05", - "sound.option.staplebops06": "Staplebops 06", - "sound.option.staplebops07": "Staplebops 07", - "sound.option.nope01": "Nope 01", - "sound.option.nope02": "Nope 02", - "sound.option.nope03": "Nope 03", - "sound.option.nope04": "Nope 04", - "sound.option.nope05": "Nope 05", - "sound.option.nope06": "Nope 06", - "sound.option.nope07": "Nope 07", - "sound.option.nope08": "Nope 08", - "sound.option.nope09": "Nope 09", - "sound.option.nope10": "Nope 10", - "sound.option.nope11": "Nope 11", - "sound.option.nope12": "Nope 12", - "sound.option.yup01": "Yup 01", - "sound.option.yup02": "Yup 02", - "sound.option.yup03": "Yup 03", - "sound.option.yup04": "Yup 04", - "sound.option.yup05": "Yup 05", - "sound.option.yup06": "Yup 06", - "settings.general.notifications.agent.title": "Agent", - "settings.general.notifications.agent.description": - "Pokaż powiadomienie systemowe, gdy agent zakończy pracę lub wymaga uwagi", - "settings.general.notifications.permissions.title": "Uprawnienia", - "settings.general.notifications.permissions.description": - "Pokaż powiadomienie systemowe, gdy wymagane jest uprawnienie", - "settings.general.notifications.errors.title": "Błędy", - "settings.general.notifications.errors.description": "Pokaż powiadomienie systemowe, gdy wystąpi błąd", - "settings.general.sounds.agent.title": "Agent", - "settings.general.sounds.agent.description": "Odtwórz dźwięk, gdy agent zakończy pracę lub wymaga uwagi", - "settings.general.sounds.permissions.title": "Uprawnienia", - "settings.general.sounds.permissions.description": "Odtwórz dźwięk, gdy wymagane jest uprawnienie", - "settings.general.sounds.errors.title": "Błędy", - "settings.general.sounds.errors.description": "Odtwórz dźwięk, gdy wystąpi błąd", - "settings.shortcuts.title": "Skróty klawiszowe", - "settings.shortcuts.reset.button": "Przywróć domyślne", - "settings.shortcuts.reset.toast.title": "Zresetowano skróty", - "settings.shortcuts.reset.toast.description": "Skróty klawiszowe zostały przywrócone do ustawień domyślnych.", - "settings.shortcuts.conflict.title": "Skrót już w użyciu", - "settings.shortcuts.conflict.description": "{{keybind}} jest już przypisany do {{titles}}.", - "settings.shortcuts.unassigned": "Nieprzypisany", - "settings.shortcuts.pressKeys": "Naciśnij klawisze", - "settings.shortcuts.search.placeholder": "Szukaj skrótów", - "settings.shortcuts.search.empty": "Nie znaleziono skrótów", - "settings.shortcuts.group.general": "Ogólne", - "settings.shortcuts.group.session": "Sesja", - "settings.shortcuts.group.navigation": "Nawigacja", - "settings.shortcuts.group.modelAndAgent": "Model i agent", - "settings.shortcuts.group.terminal": "Terminal", - "settings.shortcuts.group.prompt": "Prompt", - "settings.providers.title": "Dostawcy", - "settings.providers.description": "Ustawienia dostawców będą tutaj konfigurowalne.", - "settings.providers.section.connected": "Połączeni dostawcy", - "settings.providers.connected.empty": "Brak połączonych dostawców", - "settings.providers.section.popular": "Popularni dostawcy", - "settings.providers.tag.environment": "Środowisko", - "settings.providers.tag.config": "Konfiguracja", - "settings.providers.tag.custom": "Niestandardowe", - "settings.providers.tag.other": "Inne", - "settings.models.title": "Modele", - "settings.models.description": "Ustawienia modeli będą tutaj konfigurowalne.", - "settings.agents.title": "Agenci", - "settings.agents.description": "Ustawienia agentów będą tutaj konfigurowalne.", - "settings.commands.title": "Polecenia", - "settings.commands.description": "Ustawienia poleceń będą tutaj konfigurowalne.", - "settings.mcp.title": "MCP", - "settings.mcp.description": "Ustawienia MCP będą tutaj konfigurowalne.", - "settings.permissions.title": "Uprawnienia", - "settings.permissions.description": "Kontroluj, jakich narzędzi serwer może używać domyślnie.", - "settings.permissions.section.tools": "Narzędzia", - "settings.permissions.toast.updateFailed.title": "Nie udało się zaktualizować uprawnień", - "settings.permissions.action.allow": "Zezwól", - "settings.permissions.action.ask": "Pytaj", - "settings.permissions.action.deny": "Odmów", - "settings.permissions.tool.read.title": "Odczyt", - "settings.permissions.tool.read.description": "Odczyt pliku (pasuje do ścieżki pliku)", - "settings.permissions.tool.edit.title": "Edycja", - "settings.permissions.tool.edit.description": "Modyfikacja plików, w tym edycje, zapisy, łatki i multi-edycje", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "Dopasowywanie plików za pomocą wzorców glob", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "Przeszukiwanie zawartości plików za pomocą wyrażeń regularnych", - "settings.permissions.tool.list.title": "Lista", - "settings.permissions.tool.list.description": "Wyświetlanie listy plików w katalogu", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "Uruchamianie poleceń powłoki", - "settings.permissions.tool.task.title": "Zadanie", - "settings.permissions.tool.task.description": "Uruchamianie pod-agentów", - "settings.permissions.tool.skill.title": "Umiejętność", - "settings.permissions.tool.skill.description": "Ładowanie umiejętności według nazwy", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "Uruchamianie zapytań serwera językowego", - "settings.permissions.tool.todowrite.title": "Zapis Todo", - "settings.permissions.tool.todowrite.description": "Aktualizacja listy zadań", - "settings.permissions.tool.webfetch.title": "Pobieranie z sieci", - "settings.permissions.tool.webfetch.description": "Pobieranie zawartości z adresu URL", - "settings.permissions.tool.websearch.title": "Wyszukiwanie w sieci", - "settings.permissions.tool.websearch.description": "Przeszukiwanie sieci", - "settings.permissions.tool.codesearch.title": "Wyszukiwanie kodu", - "settings.permissions.tool.codesearch.description": "Przeszukiwanie kodu w sieci", - "settings.permissions.tool.external_directory.title": "Katalog zewnętrzny", - "settings.permissions.tool.external_directory.description": "Dostęp do plików poza katalogiem projektu", - "settings.permissions.tool.doom_loop.title": "Zapętlenie", - "settings.permissions.tool.doom_loop.description": "Wykrywanie powtarzających się wywołań narzędzi (doom loop)", - "session.delete.failed.title": "Nie udało się usunąć sesji", - "session.delete.title": "Usuń sesję", - "session.delete.confirm": 'Usunąć sesję "{{name}}"?', - "session.delete.button": "Usuń sesję", - "workspace.new": "Nowa przestrzeń robocza", - "workspace.type.local": "lokalna", - "workspace.type.sandbox": "piaskownica", - "workspace.create.failed.title": "Nie udało się utworzyć przestrzeni roboczej", - "workspace.delete.failed.title": "Nie udało się usunąć przestrzeni roboczej", - "workspace.resetting.title": "Resetowanie przestrzeni roboczej", - "workspace.resetting.description": "To może potrwać minutę.", - "workspace.reset.failed.title": "Nie udało się zresetować przestrzeni roboczej", - "workspace.reset.success.title": "Przestrzeń robocza zresetowana", - "workspace.reset.success.description": "Przestrzeń robocza odpowiada teraz domyślnej gałęzi.", - "workspace.error.stillPreparing": "Przestrzeń robocza jest wciąż przygotowywana", - "workspace.status.checking": "Sprawdzanie niezscalonych zmian...", - "workspace.status.error": "Nie można zweryfikować statusu git.", - "workspace.status.clean": "Nie wykryto niezscalonych zmian.", - "workspace.status.dirty": "Wykryto niezscalone zmiany w tej przestrzeni roboczej.", - "workspace.delete.title": "Usuń przestrzeń roboczą", - "workspace.delete.confirm": 'Usunąć przestrzeń roboczą "{{name}}"?', - "workspace.delete.button": "Usuń przestrzeń roboczą", - "workspace.reset.title": "Resetuj przestrzeń roboczą", - "workspace.reset.confirm": 'Zresetować przestrzeń roboczą "{{name}}"?', - "workspace.reset.button": "Resetuj przestrzeń roboczą", - "workspace.reset.archived.none": "Żadne aktywne sesje nie zostaną zarchiwizowane.", - "workspace.reset.archived.one": "1 sesja zostanie zarchiwizowana.", - "workspace.reset.archived.many": "{{count}} sesji zostanie zarchiwizowanych.", - "workspace.reset.note": "To zresetuje przestrzeń roboczą, aby odpowiadała domyślnej gałęzi.", - "common.open": "Otwórz", - "dialog.releaseNotes.action.getStarted": "Rozpocznij", - "dialog.releaseNotes.action.next": "Dalej", - "dialog.releaseNotes.action.hideFuture": "Nie pokazuj tego w przyszłości", - "dialog.releaseNotes.media.alt": "Podgląd wydania", - "toast.project.reloadFailed.title": "Nie udało się ponownie wczytać {{project}}", - "error.server.invalidConfiguration": "Nieprawidłowa konfiguracja", - "common.moreCountSuffix": " (jeszcze {{count}})", - "common.time.justNow": "Przed chwilą", - "common.time.minutesAgo.short": "{{count}} min temu", - "common.time.hoursAgo.short": "{{count}} godz. temu", - "common.time.daysAgo.short": "{{count}} dni temu", - "settings.providers.connected.environmentDescription": "Połączono ze zmiennymi środowiskowymi", - "settings.providers.custom.description": "Dodaj dostawcę zgodnego z OpenAI poprzez podstawowy URL.", - - "app.server.unreachable": "Nie można połączyć z {{server}}", - "app.server.retrying": "Ponawianie automatycznie...", - "app.server.otherServers": "Inne serwery", - "dialog.server.add.usernamePlaceholder": "nazwa użytkownika", - "dialog.server.add.passwordPlaceholder": "hasło", - "server.row.noUsername": "brak nazwy użytkownika", - "session.review.noVcs.createGit.title": "Utwórz repozytorium Git", - "session.review.noVcs.createGit.description": "Śledź, przeglądaj i cofaj zmiany w tym projekcie", - "session.review.noVcs.createGit.actionLoading": "Tworzenie repozytorium Git...", - "session.review.noVcs.createGit.action": "Utwórz repozytorium Git", - "session.todo.progress": "Ukończono {{done}} z {{total}} zadań", - "session.question.progress": "{{current}} z {{total}} pytań", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "Eksplorator plików", - "session.header.open.fileManager": "Menedżer plików", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "Terminal", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "Diagnostyka wydajności deweloperskiej", - "debugBar.na": "n.d.", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": - "Ostatnie zakończone przejście trasy dotykające strony sesji, mierzone od startu routera do pierwszego odrysowania po ustaleniu.", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "Średnia liczba klatek na sekundę w ciągu ostatnich 5 sekund.", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "Najgorszy czas klatki w ciągu ostatnich 5 sekund.", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "Klatki powyżej 32ms w ciągu ostatnich 5 sekund.", - "debugBar.long.label": "LONG", - "debugBar.long.tip": - "Zablokowany czas i liczba długich zadań w ciągu ostatnich 5 sekund. Maksymalne zadanie: {{max}}.", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "Najgorsze zaobserwowane opóźnienie wejścia w ciągu ostatnich 5 sekund.", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": - "Przybliżony czas trwania interakcji w ciągu ostatnich 5 sekund. Jest to podobne do INP, a nie oficjalne Web Vitals INP.", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "Skumulowane przesunięcie układu dla bieżącego czasu życia aplikacji.", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "Użyta sterta JS vs limit sterty. Tylko Chromium.", - "debugBar.mem.tip": "Użyta sterta JS vs limit sterty. {{used}} z {{limit}}.", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "Spacja", - "common.key.backspace": "Backspace", - "common.key.enter": "Enter", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "nieznany", - "error.page.circular": "[Cykliczne]", - "error.globalSDK.noServerAvailable": "Brak dostępnego serwera", - "error.globalSDK.serverNotAvailable": "Serwer niedostępny", - "error.childStore.persistedCacheCreateFailed": "Nie udało się utworzyć trwałej pamięci podręcznej", - "error.childStore.persistedProjectMetadataCreateFailed": "Nie udało się utworzyć trwałych metadanych projektu", - "error.childStore.persistedProjectIconCreateFailed": "Nie udało się utworzyć trwałej ikony projektu", - "error.childStore.storeCreateFailed": "Nie udało się utworzyć magazynu", - "terminal.connectionLost.abnormalClose": "WebSocket zamknięty nieprawidłowo: {{code}}", -} diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts deleted file mode 100644 index 6436f62c1ff..00000000000 --- a/packages/app/src/i18n/ru.ts +++ /dev/null @@ -1,938 +0,0 @@ -export const dict = { - "command.category.suggested": "Предложено", - "command.category.view": "Просмотр", - "command.category.project": "Проект", - "command.category.provider": "Провайдер", - "command.category.server": "Сервер", - "command.category.session": "Сессия", - "command.category.theme": "Тема", - "command.category.language": "Язык", - "command.category.file": "Файл", - "command.category.context": "Контекст", - "command.category.terminal": "Терминал", - "command.category.model": "Модель", - "command.category.mcp": "MCP", - "command.category.agent": "Агент", - "command.category.permissions": "Разрешения", - "command.category.workspace": "Рабочее пространство", - "command.category.settings": "Настройки", - - "theme.scheme.system": "Системная", - "theme.scheme.light": "Светлая", - "theme.scheme.dark": "Тёмная", - - "command.sidebar.toggle": "Переключить боковую панель", - "command.project.open": "Открыть проект", - "command.provider.connect": "Подключить провайдера", - "command.server.switch": "Переключить сервер", - "command.settings.open": "Открыть настройки", - "command.session.previous": "Предыдущая сессия", - "command.session.next": "Следующая сессия", - "command.session.previous.unseen": "Предыдущая непрочитанная сессия", - "command.session.next.unseen": "Следующая непрочитанная сессия", - "command.session.archive": "Архивировать сессию", - - "command.palette": "Палитра команд", - - "command.theme.cycle": "Цикл тем", - "command.theme.set": "Использовать тему: {{theme}}", - "command.theme.scheme.cycle": "Цикл цветовой схемы", - "command.theme.scheme.set": "Использовать цветовую схему: {{scheme}}", - - "command.language.cycle": "Цикл языков", - "command.language.set": "Использовать язык: {{language}}", - - "command.session.new": "Новая сессия", - "command.file.open": "Открыть файл", - "command.tab.close": "Закрыть вкладку", - "command.context.addSelection": "Добавить выделение в контекст", - "command.context.addSelection.description": "Добавить выбранные строки из текущего файла", - "command.input.focus": "Фокус на поле ввода", - "command.terminal.toggle": "Переключить терминал", - "command.fileTree.toggle": "Переключить дерево файлов", - "command.review.toggle": "Переключить обзор", - "command.terminal.new": "Новый терминал", - "command.terminal.new.description": "Создать новую вкладку терминала", - "command.steps.toggle": "Переключить шаги", - "command.steps.toggle.description": "Показать или скрыть шаги для текущего сообщения", - "command.message.previous": "Предыдущее сообщение", - "command.message.previous.description": "Перейти к предыдущему сообщению пользователя", - "command.message.next": "Следующее сообщение", - "command.message.next.description": "Перейти к следующему сообщению пользователя", - "command.model.choose": "Выбрать модель", - "command.model.choose.description": "Выбрать другую модель", - "command.mcp.toggle": "Переключить MCP", - "command.mcp.toggle.description": "Переключить MCP", - "command.agent.cycle": "Цикл агентов", - "command.agent.cycle.description": "Переключиться к следующему агенту", - "command.agent.cycle.reverse": "Цикл агентов назад", - "command.agent.cycle.reverse.description": "Переключиться к предыдущему агенту", - "command.model.variant.cycle": "Цикл режимов мышления", - "command.model.variant.cycle.description": "Переключиться к следующему уровню усилий", - "command.prompt.mode.shell": "Оболочка", - "command.prompt.mode.normal": "Промпт", - "command.permissions.autoaccept.enable": "Автоматически принимать разрешения", - "command.permissions.autoaccept.disable": "Остановить автоматическое принятие разрешений", - "command.workspace.toggle": "Переключить рабочие пространства", - "command.workspace.toggle.description": "Включить или отключить несколько рабочих пространств в боковой панели", - "command.session.undo": "Отменить", - "command.session.undo.description": "Отменить последнее сообщение", - "command.session.redo": "Повторить", - "command.session.redo.description": "Повторить отменённое сообщение", - "command.session.compact": "Сжать сессию", - "command.session.compact.description": "Сократить сессию для уменьшения размера контекста", - "command.session.fork": "Создать ответвление", - "command.session.fork.description": "Создать новую сессию из сообщения", - "command.session.share": "Поделиться сессией", - "command.session.share.description": "Поделиться сессией и скопировать URL в буфер обмена", - "command.session.unshare": "Отменить публикацию", - "command.session.unshare.description": "Прекратить публикацию сессии", - - "palette.search.placeholder": "Поиск файлов, команд и сессий", - "palette.empty": "Ничего не найдено", - "palette.group.commands": "Команды", - "palette.group.files": "Файлы", - - "dialog.provider.search.placeholder": "Поиск провайдеров", - "dialog.provider.empty": "Провайдеры не найдены", - "dialog.provider.group.popular": "Популярные", - "dialog.provider.group.other": "Другие", - "dialog.provider.tag.recommended": "Рекомендуемые", - "dialog.provider.opencode.note": "Отобранные модели, включая Claude, GPT, Gemini и другие", - "dialog.provider.opencode.tagline": "Надежные оптимизированные модели", - "dialog.provider.opencodeGo.tagline": "Доступная подписка для всех", - "dialog.provider.anthropic.note": "Прямой доступ к моделям Claude, включая Pro и Max", - "dialog.provider.copilot.note": "ИИ-модели для помощи в кодировании через GitHub Copilot", - "dialog.provider.openai.note": "Модели GPT для быстрых и мощных задач общего ИИ", - "dialog.provider.google.note": "Модели Gemini для быстрых и структурированных ответов", - "dialog.provider.openrouter.note": "Доступ ко всем поддерживаемым моделям через одного провайдера", - "dialog.provider.vercel.note": "Единый доступ к ИИ-моделям с умной маршрутизацией", - - "dialog.model.select.title": "Выбрать модель", - "dialog.model.search.placeholder": "Поиск моделей", - "dialog.model.empty": "Модели не найдены", - "dialog.model.manage": "Управление моделями", - "dialog.model.manage.description": "Настройте какие модели появляются в выборе модели", - "dialog.model.manage.provider.toggle": "Переключить все модели {{provider}}", - - "dialog.model.unpaid.freeModels.title": "Бесплатные модели от Kilo", - "dialog.model.unpaid.addMore.title": "Добавьте больше моделей от популярных провайдеров", - - "dialog.provider.viewAll": "Показать больше провайдеров", - - "provider.connect.title": "Подключить {{provider}}", - "provider.connect.title.anthropicProMax": "Войти с помощью Claude Pro/Max", - "provider.connect.selectMethod": "Выберите способ входа для {{provider}}.", - "provider.connect.method.apiKey": "API ключ", - "provider.connect.status.inProgress": "Авторизация...", - "provider.connect.status.waiting": "Ожидание авторизации...", - "provider.connect.status.failed": "Ошибка авторизации: {{error}}", - "provider.connect.apiKey.description": - "Введите ваш API ключ {{provider}} для подключения аккаунта и использования моделей {{provider}} в Kilo.", - "provider.connect.apiKey.label": "{{provider}} API ключ", - "provider.connect.apiKey.placeholder": "API ключ", - "provider.connect.apiKey.required": "API ключ обязателен", - "provider.connect.opencodeZen.line1": - "OpenCode Zen даёт вам доступ к отобранным надёжным оптимизированным моделям для агентов программирования.", - "provider.connect.opencodeZen.line2": - "С одним API ключом вы получите доступ к таким моделям как Claude, GPT, Gemini, GLM и другим.", - "provider.connect.opencodeZen.visit.prefix": "Посетите ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " чтобы получить ваш API ключ.", - "provider.connect.oauth.code.visit.prefix": "Посетите ", - "provider.connect.oauth.code.visit.link": "эту ссылку", - "provider.connect.oauth.code.visit.suffix": - " чтобы получить код авторизации для подключения аккаунта и использования моделей {{provider}} в Kilo.", - "provider.connect.oauth.code.label": "{{method}} код авторизации", - "provider.connect.oauth.code.placeholder": "Код авторизации", - "provider.connect.oauth.code.required": "Код авторизации обязателен", - "provider.connect.oauth.code.invalid": "Неверный код авторизации", - "provider.connect.oauth.auto.visit.prefix": "Посетите ", - "provider.connect.oauth.auto.visit.link": "эту ссылку", - "provider.connect.oauth.auto.visit.suffix": - " и введите код ниже для подключения аккаунта и использования моделей {{provider}} в Kilo.", - "provider.connect.oauth.auto.confirmationCode": "Код подтверждения", - "provider.connect.toast.connected.title": "{{provider}} подключён", - "provider.connect.toast.connected.description": "Модели {{provider}} теперь доступны.", - - "provider.custom.title": "Пользовательский провайдер", - "provider.custom.description.prefix": "Настройте OpenAI-совместимого провайдера. См. ", - "provider.custom.description.link": "документацию по настройке провайдера", - "provider.custom.description.suffix": ".", - "provider.custom.field.providerID.label": "ID провайдера", - "provider.custom.field.providerID.placeholder": "myprovider", - "provider.custom.field.providerID.description": "Строчные буквы, цифры, дефисы или подчёркивания", - "provider.custom.field.name.label": "Отображаемое имя", - "provider.custom.field.name.placeholder": "Мой AI провайдер", - "provider.custom.field.baseURL.label": "Базовый URL", - "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", - "provider.custom.field.apiKey.label": "API ключ", - "provider.custom.field.apiKey.placeholder": "API ключ", - "provider.custom.field.apiKey.description": - "Необязательно. Оставьте пустым, если управляете авторизацией через заголовки.", - "provider.custom.models.label": "Модели", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "model-id", - "provider.custom.models.name.label": "Имя", - "provider.custom.models.name.placeholder": "Отображаемое имя", - "provider.custom.models.remove": "Удалить модель", - "provider.custom.models.add": "Добавить модель", - "provider.custom.headers.label": "Заголовки (необязательно)", - "provider.custom.headers.key.label": "Заголовок", - "provider.custom.headers.key.placeholder": "Header-Name", - "provider.custom.headers.value.label": "Значение", - "provider.custom.headers.value.placeholder": "значение", - "provider.custom.headers.remove": "Удалить заголовок", - "provider.custom.headers.add": "Добавить заголовок", - "provider.custom.error.providerID.required": "Требуется ID провайдера", - "provider.custom.error.providerID.format": "Используйте строчные буквы, цифры, дефисы или подчёркивания", - "provider.custom.error.providerID.exists": "Такой ID провайдера уже существует", - "provider.custom.error.name.required": "Требуется отображаемое имя", - "provider.custom.error.baseURL.required": "Требуется базовый URL", - "provider.custom.error.baseURL.format": "Должен начинаться с http:// или https://", - "provider.custom.error.required": "Обязательно", - "provider.custom.error.duplicate": "Дубликат", - - "provider.disconnect.toast.disconnected.title": "{{provider}} отключён", - "provider.disconnect.toast.disconnected.description": "Модели {{provider}} больше недоступны.", - "model.tag.free": "Бесплатно", - "model.tag.latest": "Последняя", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "текст", - "model.input.image": "изображение", - "model.input.audio": "аудио", - "model.input.video": "видео", - "model.input.pdf": "pdf", - "model.tooltip.allows": "Разрешено: {{inputs}}", - "model.tooltip.reasoning.allowed": "Разрешает рассуждение", - "model.tooltip.reasoning.none": "Без рассуждения", - "model.tooltip.context": "Лимит контекста {{limit}}", - - "common.search.placeholder": "Поиск", - "common.goBack": "Назад", - "common.goForward": "Вперёд", - "common.loading": "Загрузка", - "common.loading.ellipsis": "...", - "common.cancel": "Отмена", - "common.connect": "Подключить", - "common.disconnect": "Отключить", - "common.continue": "Отправить", - "common.submit": "Отправить", - "common.save": "Сохранить", - "common.saving": "Сохранение...", - "common.default": "По умолчанию", - "common.attachment": "вложение", - - "prompt.placeholder.shell": "Введите команду оболочки... {{example}}", - "prompt.placeholder.normal": 'Спросите что угодно... "{{example}}"', - "prompt.placeholder.simple": "Спросите что угодно...", - "prompt.placeholder.summarizeComments": "Суммировать комментарии…", - "prompt.placeholder.summarizeComment": "Суммировать комментарий…", - "prompt.mode.shell": "Оболочка", - "prompt.mode.normal": "Промпт", - "prompt.mode.shell.exit": "esc для выхода", - - "prompt.example.1": "Исправить TODO в коде", - "prompt.example.2": "Какой технологический стек этого проекта?", - "prompt.example.3": "Исправить сломанные тесты", - "prompt.example.4": "Объясни как работает аутентификация", - "prompt.example.5": "Найти и исправить уязвимости безопасности", - "prompt.example.6": "Добавить юнит-тесты для сервиса пользователя", - "prompt.example.7": "Рефакторить эту функцию для лучшей читаемости", - "prompt.example.8": "Что означает эта ошибка?", - "prompt.example.9": "Помоги мне отладить эту проблему", - "prompt.example.10": "Сгенерировать документацию API", - "prompt.example.11": "Оптимизировать запросы к базе данных", - "prompt.example.12": "Добавить валидацию ввода", - "prompt.example.13": "Создать новый компонент для...", - "prompt.example.14": "Как развернуть этот проект?", - "prompt.example.15": "Проверь мой код на лучшие практики", - "prompt.example.16": "Добавить обработку ошибок в эту функцию", - "prompt.example.17": "Объясни этот паттерн regex", - "prompt.example.18": "Конвертировать это в TypeScript", - "prompt.example.19": "Добавить логирование по всему проекту", - "prompt.example.20": "Какие зависимости устарели?", - "prompt.example.21": "Помоги написать скрипт миграции", - "prompt.example.22": "Реализовать кэширование для этой конечной точки", - "prompt.example.23": "Добавить пагинацию в этот список", - "prompt.example.24": "Создать CLI команду для...", - "prompt.example.25": "Как работают переменные окружения здесь?", - - "prompt.popover.emptyResults": "Нет совпадений", - "prompt.popover.emptyCommands": "Нет совпадающих команд", - "prompt.dropzone.label": "Перетащите сюда изображения, PDF или текстовые файлы", - "prompt.dropzone.file.label": "Отпустите для @упоминания файла", - "prompt.slash.badge.custom": "своё", - "prompt.slash.badge.skill": "навык", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "активно", - "prompt.context.includeActiveFile": "Включить активный файл", - "prompt.context.removeActiveFile": "Удалить активный файл из контекста", - "prompt.context.removeFile": "Удалить файл из контекста", - "prompt.action.attachFile": "Прикрепить файл", - "prompt.attachment.remove": "Удалить вложение", - "prompt.action.send": "Отправить", - "prompt.action.stop": "Остановить", - - "prompt.toast.pasteUnsupported.title": "Неподдерживаемое вложение", - "prompt.toast.pasteUnsupported.description": "Здесь можно прикрепить только изображения, PDF или текстовые файлы.", - "prompt.toast.modelAgentRequired.title": "Выберите агента и модель", - "prompt.toast.modelAgentRequired.description": "Выберите агента и модель перед отправкой запроса.", - "prompt.toast.worktreeCreateFailed.title": "Не удалось создать worktree", - "prompt.toast.sessionCreateFailed.title": "Не удалось создать сессию", - "prompt.toast.shellSendFailed.title": "Не удалось отправить команду оболочки", - "prompt.toast.commandSendFailed.title": "Не удалось отправить команду", - "prompt.toast.promptSendFailed.title": "Не удалось отправить запрос", - "prompt.toast.promptSendFailed.description": "Не удалось получить сессию", - - "dialog.mcp.title": "MCP", - "dialog.mcp.description": "{{enabled}} из {{total}} включено", - "dialog.mcp.empty": "MCP не настроены", - - "dialog.lsp.empty": "LSP автоматически обнаружены по типам файлов", - "dialog.plugins.empty": "Плагины настроены в opencode.json", - - "mcp.status.connected": "подключено", - "mcp.status.failed": "ошибка", - "mcp.status.needs_auth": "требуется авторизация", - "mcp.status.disabled": "отключено", - - "dialog.fork.empty": "Нет сообщений для ответвления", - - "dialog.directory.search.placeholder": "Поиск папок", - "dialog.directory.empty": "Папки не найдены", - - "dialog.server.title": "Серверы", - "dialog.server.description": "Переключите сервер Kilo к которому подключается приложение.", - "dialog.server.search.placeholder": "Поиск серверов", - "dialog.server.empty": "Серверов пока нет", - "dialog.server.add.title": "Добавить сервер", - "dialog.server.add.url": "URL сервера", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "Не удалось подключиться к серверу", - "dialog.server.add.checking": "Проверка...", - "dialog.server.add.button": "Добавить сервер", - "dialog.server.add.name": "Имя сервера (необязательно)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "Имя пользователя (необязательно)", - "dialog.server.add.password": "Пароль (необязательно)", - "dialog.server.edit.title": "Редактировать сервер", - "dialog.server.default.title": "Сервер по умолчанию", - "dialog.server.default.description": - "Подключаться к этому серверу при запуске приложения вместо запуска локального сервера. Требуется перезапуск.", - "dialog.server.default.none": "Сервер не выбран", - "dialog.server.default.set": "Установить текущий сервер по умолчанию", - "dialog.server.default.clear": "Очистить", - "dialog.server.action.remove": "Удалить сервер", - - "dialog.server.menu.edit": "Редактировать", - "dialog.server.menu.default": "Сделать по умолчанию", - "dialog.server.menu.defaultRemove": "Удалить по умолчанию", - "dialog.server.menu.delete": "Удалить", - "dialog.server.current": "Текущий сервер", - "dialog.server.status.default": "По умолч.", - - "dialog.project.edit.title": "Редактировать проект", - "dialog.project.edit.name": "Название", - "dialog.project.edit.icon": "Иконка", - "dialog.project.edit.icon.alt": "Иконка проекта", - "dialog.project.edit.icon.hint": "Нажмите или перетащите изображение", - "dialog.project.edit.icon.recommended": "Рекомендуется: 128x128px", - "dialog.project.edit.color": "Цвет", - "dialog.project.edit.color.select": "Выбрать цвет {{color}}", - - "dialog.project.edit.worktree.startup": "Скрипт запуска рабочего пространства", - "dialog.project.edit.worktree.startup.description": - "Запускается после создания нового рабочего пространства (worktree).", - "dialog.project.edit.worktree.startup.placeholder": "например, bun install", - "context.breakdown.title": "Разбивка контекста", - "context.breakdown.note": - 'Приблизительная разбивка входных токенов. "Другое" включает определения инструментов и накладные расходы.', - "context.breakdown.system": "Система", - "context.breakdown.user": "Пользователь", - "context.breakdown.assistant": "Ассистент", - "context.breakdown.tool": "Вызовы инструментов", - "context.breakdown.other": "Другое", - - "context.systemPrompt.title": "Системный промпт", - "context.rawMessages.title": "Исходные сообщения", - - "context.stats.session": "Сессия", - "context.stats.messages": "Сообщения", - "context.stats.provider": "Провайдер", - "context.stats.model": "Модель", - "context.stats.limit": "Лимит контекста", - "context.stats.totalTokens": "Всего токенов", - "context.stats.usage": "Использование", - "context.stats.inputTokens": "Входные токены", - "context.stats.outputTokens": "Выходные токены", - "context.stats.reasoningTokens": "Токены рассуждения", - "context.stats.cacheTokens": "Токены кэша (чтение/запись)", - "context.stats.userMessages": "Сообщения пользователя", - "context.stats.assistantMessages": "Сообщения ассистента", - "context.stats.totalCost": "Общая стоимость", - "context.stats.sessionCreated": "Сессия создана", - "context.stats.lastActivity": "Последняя активность", - - "context.usage.tokens": "Токены", - "context.usage.usage": "Использование", - "context.usage.cost": "Стоимость", - "context.usage.clickToView": "Нажмите для просмотра контекста", - "context.usage.view": "Показать использование контекста", - - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - - "toast.language.title": "Язык", - "toast.language.description": "Переключено на {{language}}", - - "toast.theme.title": "Тема переключена", - "toast.scheme.title": "Цветовая схема", - - "toast.permissions.autoaccept.on.title": "Разрешения принимаются автоматически", - "toast.permissions.autoaccept.on.description": "Запросы на разрешения будут одобряться автоматически", - "toast.permissions.autoaccept.off.title": "Автоматическое принятие разрешений остановлено", - "toast.permissions.autoaccept.off.description": "Запросы на разрешения будут требовать одобрения", - - "toast.workspace.enabled.title": "Рабочие пространства включены", - "toast.workspace.enabled.description": "В боковой панели теперь отображаются несколько рабочих деревьев", - "toast.workspace.disabled.title": "Рабочие пространства отключены", - "toast.workspace.disabled.description": "В боковой панели отображается только главное рабочее дерево", - - "toast.model.none.title": "Модель не выбрана", - "toast.model.none.description": "Подключите провайдера для суммаризации сессии", - - "toast.file.loadFailed.title": "Не удалось загрузить файл", - - "toast.file.listFailed.title": "Не удалось получить список файлов", - "toast.context.noLineSelection.title": "Нет выделения строк", - "toast.context.noLineSelection.description": "Сначала выберите диапазон строк во вкладке файла.", - "toast.session.share.copyFailed.title": "Не удалось скопировать URL в буфер обмена", - "toast.session.share.success.title": "Сессия опубликована", - "toast.session.share.success.description": "URL скопирован в буфер обмена!", - "toast.session.share.failed.title": "Не удалось опубликовать сессию", - "toast.session.share.failed.description": "Произошла ошибка при публикации сессии", - - "toast.session.unshare.success.title": "Публикация отменена", - "toast.session.unshare.success.description": "Публикация успешно отменена!", - "toast.session.unshare.failed.title": "Не удалось отменить публикацию", - "toast.session.unshare.failed.description": "Произошла ошибка при отмене публикации", - - "toast.session.listFailed.title": "Не удалось загрузить сессии для {{project}}", - - "toast.update.title": "Доступно обновление", - "toast.update.description": "Новая версия Kilo ({{version}}) доступна для установки.", - "toast.update.action.installRestart": "Установить и перезапустить", - "toast.update.action.notYet": "Пока нет", - - "error.page.title": "Что-то пошло не так", - "error.page.description": "Произошла ошибка при загрузке приложения.", - "error.page.details.label": "Детали ошибки", - "error.page.action.restart": "Перезапустить", - "error.page.action.checking": "Проверка...", - "error.page.action.checkUpdates": "Проверить обновления", - "error.page.action.updateTo": "Обновить до {{version}}", - "error.page.report.prefix": "Пожалуйста, сообщите об этой ошибке команде Kilo", - "error.page.report.discord": "в Discord", - "error.page.version": "Версия: {{version}}", - - "error.dev.rootNotFound": - "Корневой элемент не найден. Вы забыли добавить его в index.html? Или, может быть, атрибут id был написан неправильно?", - - "error.globalSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?", - "directory.error.invalidUrl": "Недопустимая директория в URL.", - - "error.chain.unknown": "Неизвестная ошибка", - "error.chain.causedBy": "Причина:", - "error.chain.apiError": "Ошибка API", - "error.chain.status": "Статус: {{status}}", - "error.chain.retryable": "Повторная попытка: {{retryable}}", - "error.chain.responseBody": "Тело ответа:\n{{body}}", - "error.chain.didYouMean": "Возможно, вы имели в виду: {{suggestions}}", - "error.chain.modelNotFound": "Модель не найдена: {{provider}}/{{model}}", - "error.chain.checkConfig": "Проверьте названия провайдера/модели в конфиге (opencode.json)", - "error.chain.mcpFailed": - 'MCP сервер "{{name}}" завершился с ошибкой. Обратите внимание, что Kilo пока не поддерживает MCP авторизацию.', - "error.chain.providerAuthFailed": "Ошибка аутентификации провайдера ({{provider}}): {{message}}", - "error.chain.providerInitFailed": - 'Не удалось инициализировать провайдера "{{provider}}". Проверьте учётные данные и конфигурацию.', - "error.chain.configJsonInvalid": "Конфигурационный файл по адресу {{path}} не является валидным JSON(C)", - "error.chain.configJsonInvalidWithMessage": - "Конфигурационный файл по адресу {{path}} не является валидным JSON(C): {{message}}", - "error.chain.configDirectoryTypo": - 'Папка "{{dir}}" в {{path}} невалидна. Переименуйте папку в "{{suggestion}}" или удалите её. Это распространённая опечатка.', - "error.chain.configFrontmatterError": "Не удалось разобрать frontmatter в {{path}}:\n{{message}}", - "error.chain.configInvalid": "Конфигурационный файл по адресу {{path}} невалиден", - "error.chain.configInvalidWithMessage": "Конфигурационный файл по адресу {{path}} невалиден: {{message}}", - - "notification.permission.title": "Требуется разрешение", - "notification.permission.description": "{{sessionTitle}} в {{projectName}} требуется разрешение", - "notification.question.title": "Вопрос", - "notification.question.description": "У {{sessionTitle}} в {{projectName}} есть вопрос", - "notification.action.goToSession": "Перейти к сессии", - - "notification.session.responseReady.title": "Ответ готов", - "notification.session.error.title": "Ошибка сессии", - "notification.session.error.fallbackDescription": "Произошла ошибка", - - "home.recentProjects": "Недавние проекты", - "home.empty.title": "Нет недавних проектов", - "home.empty.description": "Начните с открытия локального проекта", - - "session.tab.session": "Сессия", - "session.tab.review": "Обзор", - "session.tab.context": "Контекст", - "session.panel.reviewAndFiles": "Обзор и файлы", - "session.review.filesChanged": "{{count}} файлов изменено", - "session.review.change.one": "Изменение", - "session.review.change.other": "Изменения", - "session.review.loadingChanges": "Загрузка изменений...", - "session.review.empty": "Изменений в этой сессии пока нет", - "session.review.noVcs": "Система контроля версий Git не обнаружена, изменения не отображаются", - "session.review.noSnapshot": "Отслеживание снимков отключено в настройках, поэтому изменения сессии недоступны", - "session.review.noChanges": "Нет изменений", - "session.files.selectToOpen": "Выберите файл, чтобы открыть", - "session.files.all": "Все файлы", - "session.files.empty": "Нет файлов", - "session.files.binaryContent": "Двоичный файл (содержимое не может быть отображено)", - "session.messages.renderEarlier": "Показать предыдущие сообщения", - "session.messages.loadingEarlier": "Загрузка предыдущих сообщений...", - "session.messages.loadEarlier": "Загрузить предыдущие сообщения", - "session.messages.loading": "Загрузка сообщений...", - "session.messages.jumpToLatest": "Перейти к последнему", - - "session.context.addToContext": "Добавить {{selection}} в контекст", - "session.todo.title": "Задачи", - "session.todo.collapse": "Свернуть", - "session.todo.expand": "Развернуть", - "session.followupDock.summary.one": "{{count}} сообщение в очереди", - "session.followupDock.summary.other": "{{count}} сообщений в очереди", - "session.followupDock.sendNow": "Отправить сейчас", - "session.followupDock.edit": "Редактировать", - "session.followupDock.collapse": "Свернуть сообщения в очереди", - "session.followupDock.expand": "Развернуть сообщения в очереди", - "session.revertDock.summary.one": "{{count}} сообщение возвращено", - "session.revertDock.summary.other": "{{count}} сообщений возвращено", - "session.revertDock.collapse": "Свернуть возвращённые сообщения", - "session.revertDock.expand": "Развернуть возвращённые сообщения", - "session.revertDock.restore": "Восстановить сообщение", - - "session.new.title": "Создавайте что угодно", - "session.new.worktree.main": "Основная ветка", - "session.new.worktree.mainWithBranch": "Основная ветка ({{branch}})", - "session.new.worktree.create": "Создать новый worktree", - "session.new.lastModified": "Последнее изменение", - - "session.header.search.placeholder": "Поиск {{project}}", - "session.header.searchFiles": "Поиск файлов", - "session.header.openIn": "Открыть в", - "session.header.open.action": "Открыть {{app}}", - "session.header.open.ariaLabel": "Открыть в {{app}}", - "session.header.open.menu": "Варианты открытия", - "session.header.open.copyPath": "Копировать путь", - - "status.popover.trigger": "Статус", - "status.popover.ariaLabel": "Настройки серверов", - "status.popover.tab.servers": "Серверы", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "Плагины", - "status.popover.action.manageServers": "Управлять серверами", - - "session.share.popover.title": "Опубликовать в интернете", - "session.share.popover.description.shared": - "Эта сессия общедоступна. Доступ к ней может получить любой, у кого есть ссылка.", - "session.share.popover.description.unshared": - "Опубликуйте сессию в интернете. Доступ к ней сможет получить любой, у кого есть ссылка.", - "session.share.action.share": "Поделиться", - "session.share.action.publish": "Опубликовать", - "session.share.action.publishing": "Публикация...", - "session.share.action.unpublish": "Отменить публикацию", - "session.share.action.unpublishing": "Отмена публикации...", - "session.share.action.view": "Посмотреть", - "session.share.copy.copied": "Скопировано", - "session.share.copy.copyLink": "Копировать ссылку", - - "lsp.tooltip.none": "Нет LSP серверов", - "lsp.label.connected": "{{count}} LSP", - - "prompt.loading": "Загрузка запроса...", - "terminal.loading": "Загрузка терминала...", - "terminal.title": "Терминал", - "terminal.title.numbered": "Терминал {{number}}", - "terminal.close": "Закрыть терминал", - "terminal.connectionLost.title": "Соединение потеряно", - "terminal.connectionLost.description": - "Соединение с терминалом прервано. Это может произойти при перезапуске сервера.", - - "common.closeTab": "Закрыть вкладку", - "common.dismiss": "Закрыть", - "common.requestFailed": "Запрос не выполнен", - "common.moreOptions": "Дополнительные опции", - "common.learnMore": "Подробнее", - "common.rename": "Переименовать", - "common.reset": "Сбросить", - "common.archive": "Архивировать", - "common.delete": "Удалить", - "common.close": "Закрыть", - "common.edit": "Редактировать", - "common.loadMore": "Загрузить ещё", - "common.key.esc": "ESC", - - "sidebar.menu.toggle": "Переключить меню", - "sidebar.nav.projectsAndSessions": "Проекты и сессии", - "sidebar.settings": "Настройки", - "sidebar.help": "Помощь", - "sidebar.workspaces.enable": "Включить рабочие пространства", - "sidebar.workspaces.disable": "Отключить рабочие пространства", - "sidebar.gettingStarted.title": "Начало работы", - "sidebar.gettingStarted.line1": "Kilo включает бесплатные модели, чтобы вы могли начать сразу.", - "sidebar.gettingStarted.line2": - "Подключите любого провайдера для использования моделей, включая Claude, GPT, Gemini и др.", - "sidebar.project.recentSessions": "Недавние сессии", - "sidebar.project.viewAllSessions": "Посмотреть все сессии", - "sidebar.project.clearNotifications": "Очистить уведомления", - - "app.name.desktop": "Kilo Desktop", - "settings.section.desktop": "Приложение", - "settings.section.server": "Сервер", - "settings.tab.general": "Основные", - "settings.tab.shortcuts": "Горячие клавиши", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "Интеграция с WSL", - "settings.desktop.wsl.description": "Запускать сервер Kilo внутри WSL на Windows.", - - "settings.general.section.appearance": "Внешний вид", - "settings.general.section.notifications": "Системные уведомления", - "settings.general.section.updates": "Обновления", - "settings.general.section.sounds": "Звуковые эффекты", - "settings.general.section.feed": "Лента", - "settings.general.section.display": "Дисплей", - - "settings.general.row.language.title": "Язык", - "settings.general.row.language.description": "Изменить язык отображения Kilo", - "settings.general.row.appearance.title": "Внешний вид", - "settings.general.row.appearance.description": "Настройте как Kilo выглядит на вашем устройстве", - "settings.general.row.colorScheme.title": "Цветовая схема", - "settings.general.row.colorScheme.description": "Выберите, следует ли Kilo системной, светлой или тёмной теме", - "settings.general.row.theme.title": "Тема", - "settings.general.row.theme.description": "Настройте оформление Kilo.", - "settings.general.row.font.title": "Шрифт кода", - "settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "Шрифт интерфейса", - "settings.general.row.uiFont.description": "Настройте шрифт, используемый во всем интерфейсе", - "settings.general.row.followup.title": "Поведение уточняющих вопросов", - "settings.general.row.followup.description": - "Выберите, отправлять ли уточняющие вопросы сразу или помещать их в очередь", - "settings.general.row.followup.option.queue": "Очередь", - "settings.general.row.followup.option.steer": "Направлять", - "settings.general.row.reasoningSummaries.title": "Показывать сводки рассуждений", - "settings.general.row.reasoningSummaries.description": "Отображать сводки рассуждений модели в ленте", - - "settings.general.row.shellToolPartsExpanded.title": "Разворачивать элементы инструмента shell", - "settings.general.row.shellToolPartsExpanded.description": - "Показывать элементы инструмента shell в ленте развернутыми по умолчанию", - "settings.general.row.editToolPartsExpanded.title": "Разворачивать элементы инструмента edit", - "settings.general.row.editToolPartsExpanded.description": - "Показывать элементы инструментов edit, write и patch в ленте развернутыми по умолчанию", - "settings.general.row.showSessionProgressBar.title": "Показывать индикатор прогресса сессии", - "settings.general.row.showSessionProgressBar.description": - "Показывать анимированный индикатор прогресса вверху сессии, когда агент работает", - "settings.general.row.wayland.title": "Использовать нативный Wayland", - "settings.general.row.wayland.description": "Отключить X11 fallback на Wayland. Требуется перезапуск.", - "settings.general.row.wayland.tooltip": - "На Linux с мониторами разной частоты обновления нативный Wayland может быть стабильнее.", - - "settings.general.row.releaseNotes.title": "Примечания к выпуску", - "settings.general.row.releaseNotes.description": 'Показывать всплывающие окна "Что нового" после обновлений', - - "settings.updates.row.startup.title": "Проверять обновления при запуске", - "settings.updates.row.startup.description": "Автоматически проверять обновления при запуске Kilo", - "settings.updates.row.check.title": "Проверить обновления", - "settings.updates.row.check.description": "Проверить обновления вручную и установить, если доступны", - "settings.updates.action.checkNow": "Проверить сейчас", - "settings.updates.action.checking": "Проверка...", - "settings.updates.toast.latest.title": "У вас последняя версия", - "settings.updates.toast.latest.description": "Вы используете последнюю версию Kilo.", - "sound.option.none": "Нет", - "sound.option.alert01": "Alert 01", - "sound.option.alert02": "Alert 02", - "sound.option.alert03": "Alert 03", - "sound.option.alert04": "Alert 04", - "sound.option.alert05": "Alert 05", - "sound.option.alert06": "Alert 06", - "sound.option.alert07": "Alert 07", - "sound.option.alert08": "Alert 08", - "sound.option.alert09": "Alert 09", - "sound.option.alert10": "Alert 10", - "sound.option.bipbop01": "Bip-bop 01", - "sound.option.bipbop02": "Bip-bop 02", - "sound.option.bipbop03": "Bip-bop 03", - "sound.option.bipbop04": "Bip-bop 04", - "sound.option.bipbop05": "Bip-bop 05", - "sound.option.bipbop06": "Bip-bop 06", - "sound.option.bipbop07": "Bip-bop 07", - "sound.option.bipbop08": "Bip-bop 08", - "sound.option.bipbop09": "Bip-bop 09", - "sound.option.bipbop10": "Bip-bop 10", - "sound.option.staplebops01": "Staplebops 01", - "sound.option.staplebops02": "Staplebops 02", - "sound.option.staplebops03": "Staplebops 03", - "sound.option.staplebops04": "Staplebops 04", - "sound.option.staplebops05": "Staplebops 05", - "sound.option.staplebops06": "Staplebops 06", - "sound.option.staplebops07": "Staplebops 07", - "sound.option.nope01": "Nope 01", - "sound.option.nope02": "Nope 02", - "sound.option.nope03": "Nope 03", - "sound.option.nope04": "Nope 04", - "sound.option.nope05": "Nope 05", - "sound.option.nope06": "Nope 06", - "sound.option.nope07": "Nope 07", - "sound.option.nope08": "Nope 08", - "sound.option.nope09": "Nope 09", - "sound.option.nope10": "Nope 10", - "sound.option.nope11": "Nope 11", - "sound.option.nope12": "Nope 12", - "sound.option.yup01": "Yup 01", - "sound.option.yup02": "Yup 02", - "sound.option.yup03": "Yup 03", - "sound.option.yup04": "Yup 04", - "sound.option.yup05": "Yup 05", - "sound.option.yup06": "Yup 06", - - "settings.general.notifications.agent.title": "Агент", - "settings.general.notifications.agent.description": - "Показывать системное уведомление когда агент завершён или требует внимания", - "settings.general.notifications.permissions.title": "Разрешения", - "settings.general.notifications.permissions.description": - "Показывать системное уведомление когда требуется разрешение", - "settings.general.notifications.errors.title": "Ошибки", - "settings.general.notifications.errors.description": "Показывать системное уведомление когда происходит ошибка", - - "settings.general.sounds.agent.title": "Агент", - "settings.general.sounds.agent.description": "Воспроизводить звук когда агент завершён или требует внимания", - "settings.general.sounds.permissions.title": "Разрешения", - "settings.general.sounds.permissions.description": "Воспроизводить звук когда требуется разрешение", - "settings.general.sounds.errors.title": "Ошибки", - "settings.general.sounds.errors.description": "Воспроизводить звук когда происходит ошибка", - - "settings.shortcuts.title": "Горячие клавиши", - "settings.shortcuts.reset.button": "Сбросить к умолчаниям", - "settings.shortcuts.reset.toast.title": "Горячие клавиши сброшены", - "settings.shortcuts.reset.toast.description": "Горячие клавиши были сброшены к значениям по умолчанию.", - "settings.shortcuts.conflict.title": "Сочетание уже используется", - "settings.shortcuts.conflict.description": "{{keybind}} уже назначено для {{titles}}.", - "settings.shortcuts.unassigned": "Не назначено", - "settings.shortcuts.pressKeys": "Нажмите клавиши", - "settings.shortcuts.search.placeholder": "Поиск горячих клавиш", - "settings.shortcuts.search.empty": "Горячие клавиши не найдены", - - "settings.shortcuts.group.general": "Основные", - "settings.shortcuts.group.session": "Сессия", - "settings.shortcuts.group.navigation": "Навигация", - "settings.shortcuts.group.modelAndAgent": "Модель и агент", - "settings.shortcuts.group.terminal": "Терминал", - "settings.shortcuts.group.prompt": "Запрос", - - "settings.providers.title": "Провайдеры", - "settings.providers.description": "Настройки провайдеров будут доступны здесь.", - "settings.providers.section.connected": "Подключённые провайдеры", - "settings.providers.connected.empty": "Нет подключённых провайдеров", - "settings.providers.section.popular": "Популярные провайдеры", - "settings.providers.tag.environment": "Среда", - "settings.providers.tag.config": "Конфигурация", - "settings.providers.tag.custom": "Пользовательский", - "settings.providers.tag.other": "Другое", - "settings.models.title": "Модели", - "settings.models.description": "Настройки моделей будут доступны здесь.", - "settings.agents.title": "Агенты", - "settings.agents.description": "Настройки агентов будут доступны здесь.", - "settings.commands.title": "Команды", - "settings.commands.description": "Настройки команд будут доступны здесь.", - "settings.mcp.title": "MCP", - "settings.mcp.description": "Настройки MCP будут доступны здесь.", - - "settings.permissions.title": "Разрешения", - "settings.permissions.description": "Контролируйте какие инструменты сервер может использовать по умолчанию.", - "settings.permissions.section.tools": "Инструменты", - "settings.permissions.toast.updateFailed.title": "Не удалось обновить разрешения", - - "settings.permissions.action.allow": "Разрешить", - "settings.permissions.action.ask": "Спрашивать", - "settings.permissions.action.deny": "Запретить", - - "settings.permissions.tool.read.title": "Чтение", - "settings.permissions.tool.read.description": "Чтение файла (по совпадению пути)", - "settings.permissions.tool.edit.title": "Редактирование", - "settings.permissions.tool.edit.description": - "Изменение файлов, включая редактирование, запись, патчи и мульти-редактирование", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "Сопоставление файлов по паттернам glob", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "Поиск по содержимому файлов с использованием регулярных выражений", - "settings.permissions.tool.list.title": "List", - "settings.permissions.tool.list.description": "Список файлов в директории", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "Запуск команд оболочки", - "settings.permissions.tool.task.title": "Task", - "settings.permissions.tool.task.description": "Запуск подагентов", - "settings.permissions.tool.skill.title": "Skill", - "settings.permissions.tool.skill.description": "Загрузка навыка по имени", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "Запросы к языковому серверу", - "settings.permissions.tool.todowrite.title": "Todo Write", - "settings.permissions.tool.todowrite.description": "Обновление списка задач", - "settings.permissions.tool.webfetch.title": "Web Fetch", - "settings.permissions.tool.webfetch.description": "Получение контента по URL", - "settings.permissions.tool.websearch.title": "Web Search", - "settings.permissions.tool.websearch.description": "Поиск в интернете", - "settings.permissions.tool.codesearch.title": "Code Search", - "settings.permissions.tool.codesearch.description": "Поиск кода в интернете", - "settings.permissions.tool.external_directory.title": "Внешняя директория", - "settings.permissions.tool.external_directory.description": "Доступ к файлам вне директории проекта", - "settings.permissions.tool.doom_loop.title": "Doom Loop", - "settings.permissions.tool.doom_loop.description": - "Обнаружение повторяющихся вызовов инструментов с одинаковыми входными данными", - - "session.delete.failed.title": "Не удалось удалить сессию", - "session.delete.title": "Удалить сессию", - "session.delete.confirm": 'Удалить сессию "{{name}}"?', - "session.delete.button": "Удалить сессию", - - "workspace.new": "Новое рабочее пространство", - "workspace.type.local": "локальное", - "workspace.type.sandbox": "песочница", - "workspace.create.failed.title": "Не удалось создать рабочее пространство", - "workspace.delete.failed.title": "Не удалось удалить рабочее пространство", - "workspace.resetting.title": "Сброс рабочего пространства", - "workspace.resetting.description": "Это может занять минуту.", - "workspace.reset.failed.title": "Не удалось сбросить рабочее пространство", - "workspace.reset.success.title": "Рабочее пространство сброшено", - "workspace.reset.success.description": "Рабочее пространство теперь соответствует ветке по умолчанию.", - "workspace.error.stillPreparing": "Рабочее пространство всё ещё подготавливается", - "workspace.status.checking": "Проверка незафиксированных изменений...", - "workspace.status.error": "Не удалось проверить статус git.", - "workspace.status.clean": "Незафиксированных изменений не обнаружено.", - "workspace.status.dirty": "В этом рабочем пространстве обнаружены незафиксированные изменения.", - "workspace.delete.title": "Удалить рабочее пространство", - "workspace.delete.confirm": 'Удалить рабочее пространство "{{name}}"?', - "workspace.delete.button": "Удалить рабочее пространство", - "workspace.reset.title": "Сбросить рабочее пространство", - "workspace.reset.confirm": 'Сбросить рабочее пространство "{{name}}"?', - "workspace.reset.button": "Сбросить рабочее пространство", - "workspace.reset.archived.none": "Активные сессии не будут архивированы.", - "workspace.reset.archived.one": "1 сессия будет архивирована.", - "workspace.reset.archived.many": "{{count}} сессий будет архивировано.", - "workspace.reset.note": "Это сбросит рабочее пространство до соответствия ветке по умолчанию.", - "common.open": "Открыть", - "dialog.releaseNotes.action.getStarted": "Начать", - "dialog.releaseNotes.action.next": "Далее", - "dialog.releaseNotes.action.hideFuture": "Больше не показывать", - "dialog.releaseNotes.media.alt": "Превью релиза", - "toast.project.reloadFailed.title": "Не удалось перезагрузить {{project}}", - "error.server.invalidConfiguration": "Недопустимая конфигурация", - "common.moreCountSuffix": " (ещё {{count}})", - "common.time.justNow": "Только что", - "common.time.minutesAgo.short": "{{count}} мин назад", - "common.time.hoursAgo.short": "{{count}} ч назад", - "common.time.daysAgo.short": "{{count}} д назад", - "settings.providers.connected.environmentDescription": "Подключено из ваших переменных окружения", - "settings.providers.custom.description": "Добавить провайдера, совместимого с OpenAI, по базовому URL.", - - "app.server.unreachable": "Не удалось связаться с {{server}}", - "app.server.retrying": "Автоматическая повторная попытка...", - "app.server.otherServers": "Другие серверы", - "dialog.server.add.usernamePlaceholder": "имя пользователя", - "dialog.server.add.passwordPlaceholder": "пароль", - "server.row.noUsername": "нет имени пользователя", - "session.review.noVcs.createGit.title": "Создать репозиторий Git", - "session.review.noVcs.createGit.description": "Отслеживайте, просматривайте и отменяйте изменения в этом проекте", - "session.review.noVcs.createGit.actionLoading": "Создание репозитория Git...", - "session.review.noVcs.createGit.action": "Создать репозиторий Git", - "session.todo.progress": "Выполнено {{done}} из {{total}} задач", - "session.question.progress": "{{current}} из {{total}} вопросов", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "Проводник", - "session.header.open.fileManager": "Файловый менеджер", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "Терминал", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "Диагностика производительности разработки", - "debugBar.na": "н/д", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": - "Последний завершенный переход маршрута, затрагивающий страницу сеанса, измеренный от запуска маршрутизатора до первой отрисовки после стабилизации.", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "Скользящая частота кадров в секунду за последние 5 секунд.", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "Худшее время кадра за последние 5 секунд.", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "Кадры более 32 мс за последние 5 секунд.", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "Заблокированное время и количество длинных задач за последние 5 секунд. Макс. задача: {{max}}.", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "Худшая наблюдаемая задержка ввода за последние 5 секунд.", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": - "Приблизительная продолжительность взаимодействия за последние 5 секунд. Это похоже на INP, а не официальный Web Vitals INP.", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "Кумулятивный сдвиг макета за текущее время жизни приложения.", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "Используемая куча JS по сравнению с лимитом кучи. Только Chromium.", - "debugBar.mem.tip": "Используемая куча JS по сравнению с лимитом кучи. {{used}} из {{limit}}.", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "Пробел", - "common.key.backspace": "Backspace", - "common.key.enter": "Enter", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "неизвестно", - "error.page.circular": "[Циклично]", - "error.globalSDK.noServerAvailable": "Нет доступного сервера", - "error.globalSDK.serverNotAvailable": "Сервер недоступен", - "error.childStore.persistedCacheCreateFailed": "Не удалось создать постоянный кэш", - "error.childStore.persistedProjectMetadataCreateFailed": "Не удалось создать постоянные метаданные проекта", - "error.childStore.persistedProjectIconCreateFailed": "Не удалось создать постоянный значок проекта", - "error.childStore.storeCreateFailed": "Не удалось создать хранилище", - "terminal.connectionLost.abnormalClose": "WebSocket закрыт аварийно: {{code}}", -} diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts deleted file mode 100644 index f1bc3e9f0e1..00000000000 --- a/packages/app/src/i18n/th.ts +++ /dev/null @@ -1,924 +0,0 @@ -export const dict = { - "command.category.suggested": "แนะนำ", - "command.category.view": "มุมมอง", - "command.category.project": "โปรเจกต์", - "command.category.provider": "ผู้ให้บริการ", - "command.category.server": "เซิร์ฟเวอร์", - "command.category.session": "เซสชัน", - "command.category.theme": "ธีม", - "command.category.language": "ภาษา", - "command.category.file": "ไฟล์", - "command.category.context": "บริบท", - "command.category.terminal": "เทอร์มินัล", - "command.category.model": "โมเดล", - "command.category.mcp": "MCP", - "command.category.agent": "เอเจนต์", - "command.category.permissions": "สิทธิ์", - "command.category.workspace": "พื้นที่ทำงาน", - "command.category.settings": "การตั้งค่า", - - "theme.scheme.system": "ระบบ", - "theme.scheme.light": "สว่าง", - "theme.scheme.dark": "มืด", - - "command.sidebar.toggle": "สลับแถบข้าง", - "command.project.open": "เปิดโปรเจกต์", - "command.provider.connect": "เชื่อมต่อผู้ให้บริการ", - "command.server.switch": "สลับเซิร์ฟเวอร์", - "command.settings.open": "เปิดการตั้งค่า", - "command.session.previous": "เซสชันก่อนหน้า", - "command.session.next": "เซสชันถัดไป", - "command.session.previous.unseen": "เซสชันที่ยังไม่ได้อ่านก่อนหน้า", - "command.session.next.unseen": "เซสชันที่ยังไม่ได้อ่านถัดไป", - "command.session.archive": "จัดเก็บเซสชัน", - - "command.palette": "คำสั่งค้นหา", - - "command.theme.cycle": "เปลี่ยนธีม", - "command.theme.set": "ใช้ธีม: {{theme}}", - "command.theme.scheme.cycle": "เปลี่ยนโทนสี", - "command.theme.scheme.set": "ใช้โทนสี: {{scheme}}", - - "command.language.cycle": "เปลี่ยนภาษา", - "command.language.set": "ใช้ภาษา: {{language}}", - - "command.session.new": "เซสชันใหม่", - "command.file.open": "เปิดไฟล์", - "command.tab.close": "ปิดแท็บ", - "command.context.addSelection": "เพิ่มส่วนที่เลือกไปยังบริบท", - "command.context.addSelection.description": "เพิ่มบรรทัดที่เลือกจากไฟล์ปัจจุบัน", - "command.input.focus": "โฟกัสช่องป้อนข้อมูล", - "command.terminal.toggle": "สลับเทอร์มินัล", - "command.fileTree.toggle": "สลับต้นไม้ไฟล์", - "command.review.toggle": "สลับการตรวจสอบ", - "command.terminal.new": "เทอร์มินัลใหม่", - "command.terminal.new.description": "สร้างแท็บเทอร์มินัลใหม่", - "command.steps.toggle": "สลับขั้นตอน", - "command.steps.toggle.description": "แสดงหรือซ่อนขั้นตอนสำหรับข้อความปัจจุบัน", - "command.message.previous": "ข้อความก่อนหน้า", - "command.message.previous.description": "ไปที่ข้อความผู้ใช้ก่อนหน้า", - "command.message.next": "ข้อความถัดไป", - "command.message.next.description": "ไปที่ข้อความผู้ใช้ถัดไป", - "command.model.choose": "เลือกโมเดล", - "command.model.choose.description": "เลือกโมเดลอื่น", - "command.mcp.toggle": "สลับ MCPs", - "command.mcp.toggle.description": "สลับ MCPs", - "command.agent.cycle": "เปลี่ยนเอเจนต์", - "command.agent.cycle.description": "สลับไปยังเอเจนต์ถัดไป", - "command.agent.cycle.reverse": "เปลี่ยนเอเจนต์ย้อนกลับ", - "command.agent.cycle.reverse.description": "สลับไปยังเอเจนต์ก่อนหน้า", - "command.model.variant.cycle": "เปลี่ยนความพยายามในการคิด", - "command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป", - "command.prompt.mode.shell": "เชลล์", - "command.prompt.mode.normal": "พรอมต์", - "command.permissions.autoaccept.enable": "ยอมรับสิทธิ์โดยอัตโนมัติ", - "command.permissions.autoaccept.disable": "หยุดยอมรับสิทธิ์โดยอัตโนมัติ", - "command.workspace.toggle": "สลับพื้นที่ทำงาน", - "command.workspace.toggle.description": "เปิดหรือปิดใช้งานพื้นที่ทำงานหลายรายการในแถบด้านข้าง", - "command.session.undo": "ยกเลิก", - "command.session.undo.description": "ยกเลิกข้อความล่าสุด", - "command.session.redo": "ทำซ้ำ", - "command.session.redo.description": "ทำซ้ำข้อความที่ถูกยกเลิกล่าสุด", - "command.session.compact": "บีบอัดเซสชัน", - "command.session.compact.description": "สรุปเซสชันเพื่อลดขนาดบริบท", - "command.session.fork": "แตกแขนงจากข้อความ", - "command.session.fork.description": "สร้างเซสชันใหม่จากข้อความก่อนหน้า", - "command.session.share": "แชร์เซสชัน", - "command.session.share.description": "แชร์เซสชันนี้และคัดลอก URL ไปยังคลิปบอร์ด", - "command.session.unshare": "ยกเลิกการแชร์เซสชัน", - "command.session.unshare.description": "หยุดการแชร์เซสชันนี้", - - "palette.search.placeholder": "ค้นหาไฟล์ คำสั่ง และเซสชัน", - "palette.empty": "ไม่พบผลลัพธ์", - "palette.group.commands": "คำสั่ง", - "palette.group.files": "ไฟล์", - - "dialog.provider.search.placeholder": "ค้นหาผู้ให้บริการ", - "dialog.provider.empty": "ไม่พบผู้ให้บริการ", - "dialog.provider.group.popular": "ยอดนิยม", - "dialog.provider.group.other": "อื่น ๆ", - "dialog.provider.tag.recommended": "แนะนำ", - "dialog.provider.opencode.note": "โมเดลที่คัดสรร รวมถึง Claude, GPT, Gemini และอื่น ๆ", - "dialog.provider.opencode.tagline": "โมเดลที่เชื่อถือได้และปรับให้เหมาะสม", - "dialog.provider.opencodeGo.tagline": "การสมัครสมาชิกราคาประหยัดสำหรับทุกคน", - "dialog.provider.anthropic.note": "เข้าถึงโมเดล Claude โดยตรง รวมถึง Pro และ Max", - "dialog.provider.copilot.note": "โมเดล AI สำหรับการช่วยเหลือในการเขียนโค้ดผ่าน GitHub Copilot", - "dialog.provider.openai.note": "โมเดล GPT สำหรับงาน AI ทั่วไปที่รวดเร็วและมีความสามารถ", - "dialog.provider.google.note": "โมเดล Gemini สำหรับการตอบสนองที่รวดเร็วและมีโครงสร้าง", - "dialog.provider.openrouter.note": "เข้าถึงโมเดลที่รองรับทั้งหมดจากผู้ให้บริการเดียว", - "dialog.provider.vercel.note": "การเข้าถึงโมเดล AI แบบรวมด้วยการกำหนดเส้นทางอัจฉริยะ", - - "dialog.model.select.title": "เลือกโมเดล", - "dialog.model.search.placeholder": "ค้นหาโมเดล", - "dialog.model.empty": "ไม่พบผลลัพธ์โมเดล", - "dialog.model.manage": "จัดการโมเดล", - "dialog.model.manage.description": "ปรับแต่งโมเดลที่จะปรากฏในตัวเลือกโมเดล", - "dialog.model.manage.provider.toggle": "สลับโมเดลทั้งหมดของ {{provider}}", - - "dialog.model.unpaid.freeModels.title": "โมเดลฟรีที่จัดหาให้โดย Kilo", - "dialog.model.unpaid.addMore.title": "เพิ่มโมเดลเพิ่มเติมจากผู้ให้บริการยอดนิยม", - - "dialog.provider.viewAll": "แสดงผู้ให้บริการเพิ่มเติม", - - "provider.connect.title": "เชื่อมต่อ {{provider}}", - "provider.connect.title.anthropicProMax": "เข้าสู่ระบบด้วย Claude Pro/Max", - "provider.connect.selectMethod": "เลือกวิธีการเข้าสู่ระบบสำหรับ {{provider}}", - "provider.connect.method.apiKey": "คีย์ API", - "provider.connect.status.inProgress": "กำลังอนุญาต...", - "provider.connect.status.waiting": "รอการอนุญาต...", - "provider.connect.status.failed": "การอนุญาตล้มเหลว: {{error}}", - "provider.connect.apiKey.description": - "ป้อนคีย์ API ของ {{provider}} เพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo", - "provider.connect.apiKey.label": "คีย์ API ของ {{provider}}", - "provider.connect.apiKey.placeholder": "คีย์ API", - "provider.connect.apiKey.required": "ต้องใช้คีย์ API", - "provider.connect.opencodeZen.line1": - "OpenCode Zen ให้คุณเข้าถึงชุดโมเดลที่เชื่อถือได้และปรับแต่งแล้วสำหรับเอเจนต์การเขียนโค้ด", - "provider.connect.opencodeZen.line2": - "ด้วยคีย์ API เดียวคุณจะได้รับการเข้าถึงโมเดล เช่น Claude, GPT, Gemini, GLM และอื่น ๆ", - "provider.connect.opencodeZen.visit.prefix": "เยี่ยมชม ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " เพื่อรวบรวมคีย์ API ของคุณ", - "provider.connect.oauth.code.visit.prefix": "เยี่ยมชม ", - "provider.connect.oauth.code.visit.link": "ลิงก์นี้", - "provider.connect.oauth.code.visit.suffix": - " เพื่อรวบรวมรหัสการอนุญาตของคุณเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo", - "provider.connect.oauth.code.label": "รหัสการอนุญาต {{method}}", - "provider.connect.oauth.code.placeholder": "รหัสการอนุญาต", - "provider.connect.oauth.code.required": "ต้องใช้รหัสการอนุญาต", - "provider.connect.oauth.code.invalid": "รหัสการอนุญาตไม่ถูกต้อง", - "provider.connect.oauth.auto.visit.prefix": "เยี่ยมชม ", - "provider.connect.oauth.auto.visit.link": "ลิงก์นี้", - "provider.connect.oauth.auto.visit.suffix": " และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo", - "provider.connect.oauth.auto.confirmationCode": "รหัสยืนยัน", - "provider.connect.toast.connected.title": "{{provider}} ที่เชื่อมต่อแล้ว", - "provider.connect.toast.connected.description": "โมเดล {{provider}} พร้อมใช้งานแล้ว", - - "provider.custom.title": "ผู้ให้บริการที่กำหนดเอง", - "provider.custom.description.prefix": "กำหนดค่าผู้ให้บริการที่เข้ากันได้กับ OpenAI ดู ", - "provider.custom.description.link": "เอกสารการกำหนดค่าผู้ให้บริการ", - "provider.custom.description.suffix": ".", - "provider.custom.field.providerID.label": "รหัสผู้ให้บริการ", - "provider.custom.field.providerID.placeholder": "myprovider", - "provider.custom.field.providerID.description": "ตัวอักษรพิมพ์เล็ก ตัวเลข ยัติภังค์ หรือขีดล่าง", - "provider.custom.field.name.label": "ชื่อที่แสดง", - "provider.custom.field.name.placeholder": "My AI Provider", - "provider.custom.field.baseURL.label": "URL พื้นฐาน", - "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", - "provider.custom.field.apiKey.label": "คีย์ API", - "provider.custom.field.apiKey.placeholder": "คีย์ API", - "provider.custom.field.apiKey.description": "ไม่บังคับ เว้นว่างไว้หากคุณจัดการการยืนยันตัวตนผ่านส่วนหัว", - "provider.custom.models.label": "โมเดล", - "provider.custom.models.id.label": "รหัส", - "provider.custom.models.id.placeholder": "model-id", - "provider.custom.models.name.label": "ชื่อ", - "provider.custom.models.name.placeholder": "ชื่อที่แสดง", - "provider.custom.models.remove": "ลบโมเดล", - "provider.custom.models.add": "เพิ่มโมเดล", - "provider.custom.headers.label": "ส่วนหัว (ไม่บังคับ)", - "provider.custom.headers.key.label": "ส่วนหัว", - "provider.custom.headers.key.placeholder": "Header-Name", - "provider.custom.headers.value.label": "ค่า", - "provider.custom.headers.value.placeholder": "ค่า", - "provider.custom.headers.remove": "ลบส่วนหัว", - "provider.custom.headers.add": "เพิ่มส่วนหัว", - "provider.custom.error.providerID.required": "ต้องระบุรหัสผู้ให้บริการ", - "provider.custom.error.providerID.format": "ใช้ตัวอักษรพิมพ์เล็ก ตัวเลข ยัติภังค์ หรือขีดล่าง", - "provider.custom.error.providerID.exists": "รหัสผู้ให้บริการนั้นมีอยู่แล้ว", - "provider.custom.error.name.required": "ต้องระบุชื่อที่แสดง", - "provider.custom.error.baseURL.required": "ต้องระบุ URL พื้นฐาน", - "provider.custom.error.baseURL.format": "ต้องขึ้นต้นด้วย http:// หรือ https://", - "provider.custom.error.required": "จำเป็น", - "provider.custom.error.duplicate": "ซ้ำ", - - "provider.disconnect.toast.disconnected.title": "{{provider}} ที่ยกเลิกการเชื่อมต่อแล้ว", - "provider.disconnect.toast.disconnected.description": "โมเดล {{provider}} ไม่พร้อมใช้งานอีกต่อไป", - - "model.tag.free": "ฟรี", - "model.tag.latest": "ล่าสุด", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "ข้อความ", - "model.input.image": "รูปภาพ", - "model.input.audio": "เสียง", - "model.input.video": "วิดีโอ", - "model.input.pdf": "PDF", - "model.tooltip.allows": "อนุญาต: {{inputs}}", - "model.tooltip.reasoning.allowed": "อนุญาตการใช้เหตุผล", - "model.tooltip.reasoning.none": "ไม่มีการใช้เหตุผล", - "model.tooltip.context": "ขีดจำกัดบริบท {{limit}}", - - "common.search.placeholder": "ค้นหา", - "common.goBack": "ย้อนกลับ", - "common.goForward": "นำทางไปข้างหน้า", - "common.loading": "กำลังโหลด", - "common.loading.ellipsis": "...", - "common.cancel": "ยกเลิก", - "common.connect": "เชื่อมต่อ", - "common.disconnect": "ยกเลิกการเชื่อมต่อ", - "common.continue": "ส่ง", - "common.submit": "ส่ง", - "common.save": "บันทึก", - "common.saving": "กำลังบันทึก...", - "common.default": "ค่าเริ่มต้น", - "common.attachment": "ไฟล์แนบ", - - "prompt.placeholder.shell": "ป้อนคำสั่งเชลล์... {{example}}", - "prompt.placeholder.normal": 'ถามอะไรก็ได้... "{{example}}"', - "prompt.placeholder.simple": "ถามอะไรก็ได้...", - "prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…", - "prompt.placeholder.summarizeComment": "สรุปความคิดเห็น…", - "prompt.mode.shell": "เชลล์", - "prompt.mode.normal": "พรอมต์", - "prompt.mode.shell.exit": "กด esc เพื่อออก", - - "prompt.example.1": "แก้ไข TODO ในโค้ดเบส", - "prompt.example.2": "เทคโนโลยีของโปรเจกต์นี้คืออะไร?", - "prompt.example.3": "แก้ไขการทดสอบที่เสีย", - "prompt.example.4": "อธิบายวิธีการทำงานของการตรวจสอบสิทธิ์", - "prompt.example.5": "ค้นหาและแก้ไขช่องโหว่ความปลอดภัย", - "prompt.example.6": "เพิ่มการทดสอบหน่วยสำหรับบริการผู้ใช้", - "prompt.example.7": "ปรับโครงสร้างฟังก์ชันนี้ให้อ่านง่ายขึ้น", - "prompt.example.8": "ข้อผิดพลาดนี้หมายความว่าอะไร?", - "prompt.example.9": "ช่วยฉันดีบักปัญหานี้", - "prompt.example.10": "สร้างเอกสาร API", - "prompt.example.11": "ปรับปรุงการสืบค้นฐานข้อมูล", - "prompt.example.12": "เพิ่มการตรวจสอบข้อมูลนำเข้า", - "prompt.example.13": "สร้างคอมโพเนนต์ใหม่สำหรับ...", - "prompt.example.14": "ฉันจะทำให้โปรเจกต์นี้ทำงานได้อย่างไร?", - "prompt.example.15": "ตรวจสอบโค้ดของฉันเพื่อแนวทางปฏิบัติที่ดีที่สุด", - "prompt.example.16": "เพิ่มการจัดการข้อผิดพลาดในฟังก์ชันนี้", - "prompt.example.17": "อธิบายรูปแบบ regex นี้", - "prompt.example.18": "แปลงสิ่งนี้เป็น TypeScript", - "prompt.example.19": "เพิ่มการบันทึกทั่วทั้งโค้ดเบส", - "prompt.example.20": "มีการพึ่งพาอะไรที่ล้าสมัยอยู่?", - "prompt.example.21": "ช่วยฉันเขียนสคริปต์การย้ายข้อมูล", - "prompt.example.22": "ใช้งานแคชสำหรับจุดสิ้นสุดนี้", - "prompt.example.23": "เพิ่มการแบ่งหน้าในรายการนี้", - "prompt.example.24": "สร้างคำสั่ง CLI สำหรับ...", - "prompt.example.25": "ตัวแปรสภาพแวดล้อมทำงานอย่างไรที่นี่?", - - "prompt.popover.emptyResults": "ไม่พบผลลัพธ์ที่ตรงกัน", - "prompt.popover.emptyCommands": "ไม่พบคำสั่งที่ตรงกัน", - "prompt.dropzone.label": "ลากรูปภาพ, PDF หรือไฟล์ข้อความมาวางที่นี่", - "prompt.dropzone.file.label": "วางเพื่อ @กล่าวถึงไฟล์", - "prompt.slash.badge.custom": "กำหนดเอง", - "prompt.slash.badge.skill": "ทักษะ", - "prompt.slash.badge.mcp": "MCP", - "prompt.context.active": "ใช้งานอยู่", - "prompt.context.includeActiveFile": "รวมไฟล์ที่ใช้งานอยู่", - "prompt.context.removeActiveFile": "เอาไฟล์ที่ใช้งานอยู่ออกจากบริบท", - "prompt.context.removeFile": "เอาไฟล์ออกจากบริบท", - "prompt.action.attachFile": "แนบไฟล์", - "prompt.attachment.remove": "เอาไฟล์แนบออก", - "prompt.action.send": "ส่ง", - "prompt.action.stop": "หยุด", - - "prompt.toast.pasteUnsupported.title": "ไฟล์แนบที่ไม่รองรับ", - "prompt.toast.pasteUnsupported.description": "แนบได้เฉพาะรูปภาพ, PDF หรือไฟล์ข้อความเท่านั้น", - "prompt.toast.modelAgentRequired.title": "เลือกเอเจนต์และโมเดล", - "prompt.toast.modelAgentRequired.description": "เลือกเอเจนต์และโมเดลก่อนส่งพร้อมท์", - "prompt.toast.worktreeCreateFailed.title": "ไม่สามารถสร้าง worktree", - "prompt.toast.sessionCreateFailed.title": "ไม่สามารถสร้างเซสชัน", - "prompt.toast.shellSendFailed.title": "ไม่สามารถส่งคำสั่งเชลล์", - "prompt.toast.commandSendFailed.title": "ไม่สามารถส่งคำสั่ง", - "prompt.toast.promptSendFailed.title": "ไม่สามารถส่งพร้อมท์", - "prompt.toast.promptSendFailed.description": "ไม่สามารถดึงเซสชันได้", - - "dialog.mcp.title": "MCPs", - "dialog.mcp.description": "{{enabled}} จาก {{total}} ที่เปิดใช้งาน", - "dialog.mcp.empty": "ไม่มี MCP ที่กำหนดค่า", - - "dialog.lsp.empty": "LSPs ตรวจจับอัตโนมัติจากประเภทไฟล์", - "dialog.plugins.empty": "ปลั๊กอินที่กำหนดค่าใน opencode.json", - - "mcp.status.connected": "เชื่อมต่อแล้ว", - "mcp.status.failed": "ล้มเหลว", - "mcp.status.needs_auth": "ต้องการการตรวจสอบสิทธิ์", - "mcp.status.disabled": "ปิดใช้งาน", - - "dialog.fork.empty": "ไม่มีข้อความให้แตกแขนง", - - "dialog.directory.search.placeholder": "ค้นหาโฟลเดอร์", - "dialog.directory.empty": "ไม่พบโฟลเดอร์", - - "dialog.server.title": "เซิร์ฟเวอร์", - "dialog.server.description": "สลับเซิร์ฟเวอร์ Kilo ที่แอปนี้เชื่อมต่อด้วย", - "dialog.server.search.placeholder": "ค้นหาเซิร์ฟเวอร์", - "dialog.server.empty": "ยังไม่มีเซิร์ฟเวอร์", - "dialog.server.add.title": "เพิ่มเซิร์ฟเวอร์", - "dialog.server.add.url": "URL เซิร์ฟเวอร์", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์", - "dialog.server.add.checking": "กำลังตรวจสอบ...", - "dialog.server.add.button": "เพิ่มเซิร์ฟเวอร์", - "dialog.server.add.name": "ชื่อเซิร์ฟเวอร์ (ไม่บังคับ)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "ชื่อผู้ใช้ (ไม่บังคับ)", - "dialog.server.add.password": "รหัสผ่าน (ไม่บังคับ)", - "dialog.server.edit.title": "แก้ไขเซิร์ฟเวอร์", - "dialog.server.default.title": "เซิร์ฟเวอร์เริ่มต้น", - "dialog.server.default.description": - "เชื่อมต่อกับเซิร์ฟเวอร์นี้เมื่อเปิดแอปแทนการเริ่มเซิร์ฟเวอร์ในเครื่อง ต้องรีสตาร์ท", - "dialog.server.default.none": "ไม่ได้เลือกเซิร์ฟเวอร์", - "dialog.server.default.set": "ตั้งเซิร์ฟเวอร์ปัจจุบันเป็นค่าเริ่มต้น", - "dialog.server.default.clear": "ล้าง", - "dialog.server.action.remove": "เอาเซิร์ฟเวอร์ออก", - - "dialog.server.menu.edit": "แก้ไข", - "dialog.server.menu.default": "ตั้งเป็นค่าเริ่มต้น", - "dialog.server.menu.defaultRemove": "เอาค่าเริ่มต้นออก", - "dialog.server.menu.delete": "ลบ", - "dialog.server.current": "เซิร์ฟเวอร์ปัจจุบัน", - "dialog.server.status.default": "ค่าเริ่มต้น", - - "dialog.project.edit.title": "แก้ไขโปรเจกต์", - "dialog.project.edit.name": "ชื่อ", - "dialog.project.edit.icon": "ไอคอน", - "dialog.project.edit.icon.alt": "ไอคอนโปรเจกต์", - "dialog.project.edit.icon.hint": "คลิกหรือลากรูปภาพ", - "dialog.project.edit.icon.recommended": "แนะนำ: 128x128px", - "dialog.project.edit.color": "สี", - "dialog.project.edit.color.select": "เลือกสี {{color}}", - "dialog.project.edit.worktree.startup": "สคริปต์เริ่มต้นพื้นที่ทำงาน", - "dialog.project.edit.worktree.startup.description": "ทำงานหลังจากสร้างพื้นที่ทำงานใหม่ (worktree)", - "dialog.project.edit.worktree.startup.placeholder": "เช่น bun install", - - "context.breakdown.title": "การแบ่งบริบท", - "context.breakdown.note": 'การแบ่งโดยประมาณของโทเค็นนำเข้า "อื่น ๆ" รวมถึงคำนิยามเครื่องมือและโอเวอร์เฮด', - "context.breakdown.system": "ระบบ", - "context.breakdown.user": "ผู้ใช้", - "context.breakdown.assistant": "ผู้ช่วย", - "context.breakdown.tool": "การเรียกเครื่องมือ", - "context.breakdown.other": "อื่น ๆ", - - "context.systemPrompt.title": "พร้อมท์ระบบ", - "context.rawMessages.title": "ข้อความดิบ", - - "context.stats.session": "เซสชัน", - "context.stats.messages": "ข้อความ", - "context.stats.provider": "ผู้ให้บริการ", - "context.stats.model": "โมเดล", - "context.stats.limit": "ขีดจำกัดบริบท", - "context.stats.totalTokens": "โทเค็นทั้งหมด", - "context.stats.usage": "การใช้งาน", - "context.stats.inputTokens": "โทเค็นนำเข้า", - "context.stats.outputTokens": "โทเค็นส่งออก", - "context.stats.reasoningTokens": "โทเค็นการใช้เหตุผล", - "context.stats.cacheTokens": "โทเค็นแคช (อ่าน/เขียน)", - "context.stats.userMessages": "ข้อความผู้ใช้", - "context.stats.assistantMessages": "ข้อความผู้ช่วย", - "context.stats.totalCost": "ต้นทุนทั้งหมด", - "context.stats.sessionCreated": "สร้างเซสชันเมื่อ", - "context.stats.lastActivity": "กิจกรรมล่าสุด", - - "context.usage.tokens": "โทเค็น", - "context.usage.usage": "การใช้งาน", - "context.usage.cost": "ต้นทุน", - "context.usage.clickToView": "คลิกเพื่อดูบริบท", - "context.usage.view": "ดูการใช้บริบท", - - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - - "toast.language.title": "ภาษา", - "toast.language.description": "สลับไปที่ {{language}}", - - "toast.theme.title": "สลับธีมแล้ว", - "toast.scheme.title": "โทนสี", - - "toast.workspace.enabled.title": "เปิดใช้งานพื้นที่ทำงานแล้ว", - "toast.workspace.enabled.description": "ตอนนี้จะแสดง worktree หลายรายการในแถบด้านข้าง", - "toast.workspace.disabled.title": "ปิดใช้งานพื้นที่ทำงานแล้ว", - "toast.workspace.disabled.description": "จะแสดงเฉพาะ worktree หลักในแถบด้านข้าง", - - "toast.permissions.autoaccept.on.title": "กำลังยอมรับสิทธิ์โดยอัตโนมัติ", - "toast.permissions.autoaccept.on.description": "คำขอสิทธิ์จะได้รับการอนุมัติโดยอัตโนมัติ", - "toast.permissions.autoaccept.off.title": "หยุดยอมรับสิทธิ์โดยอัตโนมัติแล้ว", - "toast.permissions.autoaccept.off.description": "คำขอสิทธิ์จะต้องได้รับการอนุมัติ", - - "toast.model.none.title": "ไม่ได้เลือกโมเดล", - "toast.model.none.description": "เชื่อมต่อผู้ให้บริการเพื่อสรุปเซสชันนี้", - - "toast.file.loadFailed.title": "ไม่สามารถโหลดไฟล์", - "toast.file.listFailed.title": "ไม่สามารถแสดงรายการไฟล์", - - "toast.context.noLineSelection.title": "ไม่มีการเลือกบรรทัด", - "toast.context.noLineSelection.description": "เลือกช่วงบรรทัดในแท็บไฟล์ก่อน", - - "toast.session.share.copyFailed.title": "ไม่สามารถคัดลอก URL ไปยังคลิปบอร์ด", - "toast.session.share.success.title": "แชร์เซสชันแล้ว", - "toast.session.share.success.description": "คัดลอก URL แชร์ไปยังคลิปบอร์ดแล้ว!", - "toast.session.share.failed.title": "ไม่สามารถแชร์เซสชัน", - "toast.session.share.failed.description": "เกิดข้อผิดพลาดระหว่างการแชร์เซสชัน", - - "toast.session.unshare.success.title": "ยกเลิกการแชร์เซสชันแล้ว", - "toast.session.unshare.success.description": "ยกเลิกการแชร์เซสชันสำเร็จ!", - "toast.session.unshare.failed.title": "ไม่สามารถยกเลิกการแชร์เซสชัน", - "toast.session.unshare.failed.description": "เกิดข้อผิดพลาดระหว่างการยกเลิกการแชร์เซสชัน", - - "toast.session.listFailed.title": "ไม่สามารถโหลดเซสชันสำหรับ {{project}}", - - "toast.update.title": "มีการอัปเดต", - "toast.update.description": "เวอร์ชันใหม่ของ Kilo ({{version}}) พร้อมใช้งานสำหรับติดตั้ง", - "toast.update.action.installRestart": "ติดตั้งและรีสตาร์ท", - "toast.update.action.notYet": "ยังไม่", - - "error.page.title": "เกิดข้อผิดพลาด", - "error.page.description": "เกิดข้อผิดพลาดระหว่างการโหลดแอปพลิเคชัน", - "error.page.details.label": "รายละเอียดข้อผิดพลาด", - "error.page.action.restart": "รีสตาร์ท", - "error.page.action.checking": "กำลังตรวจสอบ...", - "error.page.action.checkUpdates": "ตรวจสอบการอัปเดต", - "error.page.action.updateTo": "อัปเดตเป็น {{version}}", - "error.page.report.prefix": "โปรดรายงานข้อผิดพลาดนี้ให้ทีม Kilo", - "error.page.report.discord": "บน Discord", - "error.page.version": "เวอร์ชัน: {{version}}", - - "error.dev.rootNotFound": "ไม่พบองค์ประกอบรูท คุณลืมเพิ่มใน index.html หรือบางทีแอตทริบิวต์ id อาจสะกดผิด?", - - "error.globalSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?", - "directory.error.invalidUrl": "ไดเรกทอรีใน URL ไม่ถูกต้อง", - - "error.chain.unknown": "ข้อผิดพลาดที่ไม่รู้จัก", - "error.chain.causedBy": "สาเหตุ:", - "error.chain.apiError": "ข้อผิดพลาด API", - "error.chain.status": "สถานะ: {{status}}", - "error.chain.retryable": "สามารถลองใหม่: {{retryable}}", - "error.chain.responseBody": "เนื้อหาการตอบสนอง:\n{{body}}", - "error.chain.didYouMean": "คุณหมายถึง: {{suggestions}}", - "error.chain.modelNotFound": "ไม่พบโมเดล: {{provider}}/{{model}}", - "error.chain.checkConfig": "ตรวจสอบการกำหนดค่าของคุณ (opencode.json) ชื่อผู้ให้บริการ/โมเดล", - "error.chain.mcpFailed": 'เซิร์ฟเวอร์ MCP "{{name}}" ล้มเหลว โปรดทราบว่า Kilo ยังไม่รองรับการตรวจสอบสิทธิ์ MCP', - "error.chain.providerAuthFailed": "การตรวจสอบสิทธิ์ผู้ให้บริการล้มเหลว ({{provider}}): {{message}}", - "error.chain.providerInitFailed": 'ไม่สามารถเริ่มต้นผู้ให้บริการ "{{provider}}" ตรวจสอบข้อมูลรับรองและการกำหนดค่า', - "error.chain.configJsonInvalid": "ไฟล์กำหนดค่าที่ {{path}} ไม่ใช่ JSON(C) ที่ถูกต้อง", - "error.chain.configJsonInvalidWithMessage": "ไฟล์กำหนดค่าที่ {{path}} ไม่ใช่ JSON(C) ที่ถูกต้อง: {{message}}", - "error.chain.configDirectoryTypo": - 'ไดเรกทอรี "{{dir}}" ใน {{path}} ไม่ถูกต้อง เปลี่ยนชื่อไดเรกทอรีเป็น "{{suggestion}}" หรือเอาออก นี่เป็นการสะกดผิดทั่วไป', - "error.chain.configFrontmatterError": "ไม่สามารถแยกวิเคราะห์ frontmatter ใน {{path}}:\n{{message}}", - "error.chain.configInvalid": "ไฟล์กำหนดค่าที่ {{path}} ไม่ถูกต้อง", - "error.chain.configInvalidWithMessage": "ไฟล์กำหนดค่าที่ {{path}} ไม่ถูกต้อง: {{message}}", - - "notification.permission.title": "ต้องการสิทธิ์", - "notification.permission.description": "{{sessionTitle}} ใน {{projectName}} ต้องการสิทธิ์", - "notification.question.title": "คำถาม", - "notification.question.description": "{{sessionTitle}} ใน {{projectName}} มีคำถาม", - "notification.action.goToSession": "ไปที่เซสชัน", - - "notification.session.responseReady.title": "การตอบสนองพร้อม", - "notification.session.error.title": "ข้อผิดพลาดเซสชัน", - "notification.session.error.fallbackDescription": "เกิดข้อผิดพลาด", - - "home.recentProjects": "โปรเจกต์ล่าสุด", - "home.empty.title": "ไม่มีโปรเจกต์ล่าสุด", - "home.empty.description": "เริ่มต้นโดยเปิดโปรเจกต์ในเครื่อง", - - "session.tab.session": "เซสชัน", - "session.tab.review": "ตรวจสอบ", - "session.tab.context": "บริบท", - "session.panel.reviewAndFiles": "ตรวจสอบและไฟล์", - "session.review.filesChanged": "{{count}} ไฟล์ที่เปลี่ยนแปลง", - "session.review.change.one": "การเปลี่ยนแปลง", - "session.review.change.other": "การเปลี่ยนแปลง", - "session.review.loadingChanges": "กำลังโหลดการเปลี่ยนแปลง...", - "session.review.empty": "ยังไม่มีการเปลี่ยนแปลงในเซสชันนี้", - "session.review.noVcs": "ไม่ตรวจพบระบบควบคุมเวอร์ชัน Git การเปลี่ยนแปลงจะไม่แสดง", - "session.review.noSnapshot": "การติดตามสแนปชอตถูกปิดใช้งานในการกำหนดค่า ดังนั้นการเปลี่ยนแปลงเซสชันจึงไม่พร้อมใช้งาน", - "session.review.noChanges": "ไม่มีการเปลี่ยนแปลง", - - "session.files.selectToOpen": "เลือกไฟล์เพื่อเปิด", - "session.files.empty": "ไม่มีไฟล์", - "session.files.all": "ไฟล์ทั้งหมด", - "session.files.binaryContent": "ไฟล์ไบนารี (ไม่สามารถแสดงเนื้อหาได้)", - - "session.messages.renderEarlier": "แสดงข้อความก่อนหน้า", - "session.messages.loadingEarlier": "กำลังโหลดข้อความก่อนหน้า...", - "session.messages.loadEarlier": "โหลดข้อความก่อนหน้า", - "session.messages.loading": "กำลังโหลดข้อความ...", - "session.messages.jumpToLatest": "ไปที่ล่าสุด", - - "session.context.addToContext": "เพิ่ม {{selection}} ไปยังบริบท", - "session.todo.title": "สิ่งที่ต้องทำ", - "session.todo.collapse": "ย่อ", - "session.todo.expand": "ขยาย", - "session.followupDock.summary.one": "{{count}} ข้อความในคิว", - "session.followupDock.summary.other": "{{count}} ข้อความในคิว", - "session.followupDock.sendNow": "ส่งทันที", - "session.followupDock.edit": "แก้ไข", - "session.followupDock.collapse": "ย่อข้อความในคิว", - "session.followupDock.expand": "ขยายข้อความในคิว", - "session.revertDock.summary.one": "{{count}} ข้อความที่ถูกย้อนกลับ", - "session.revertDock.summary.other": "{{count}} ข้อความที่ถูกย้อนกลับ", - "session.revertDock.collapse": "ย่อข้อความที่ถูกย้อนกลับ", - "session.revertDock.expand": "ขยายข้อความที่ถูกย้อนกลับ", - "session.revertDock.restore": "กู้คืนข้อความ", - - "session.new.title": "สร้างอะไรก็ได้", - "session.new.worktree.main": "สาขาหลัก", - "session.new.worktree.mainWithBranch": "สาขาหลัก ({{branch}})", - "session.new.worktree.create": "สร้าง worktree ใหม่", - "session.new.lastModified": "แก้ไขล่าสุด", - - "session.header.search.placeholder": "ค้นหา {{project}}", - "session.header.searchFiles": "ค้นหาไฟล์", - "session.header.openIn": "เปิดใน", - "session.header.open.action": "เปิด {{app}}", - "session.header.open.ariaLabel": "เปิดใน {{app}}", - "session.header.open.menu": "ตัวเลือกการเปิด", - "session.header.open.copyPath": "คัดลอกเส้นทาง", - - "status.popover.trigger": "สถานะ", - "status.popover.ariaLabel": "การกำหนดค่าเซิร์ฟเวอร์", - "status.popover.tab.servers": "เซิร์ฟเวอร์", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "ปลั๊กอิน", - "status.popover.action.manageServers": "จัดการเซิร์ฟเวอร์", - - "session.share.popover.title": "เผยแพร่บนเว็บ", - "session.share.popover.description.shared": "เซสชันนี้เป็นสาธารณะบนเว็บ สามารถเข้าถึงได้โดยผู้ที่มีลิงก์", - "session.share.popover.description.unshared": "แชร์เซสชันสาธารณะบนเว็บ จะเข้าถึงได้โดยผู้ที่มีลิงก์", - "session.share.action.share": "แชร์", - "session.share.action.publish": "เผยแพร่", - "session.share.action.publishing": "กำลังเผยแพร่...", - "session.share.action.unpublish": "ยกเลิกการเผยแพร่", - "session.share.action.unpublishing": "กำลังยกเลิกการเผยแพร่...", - "session.share.action.view": "ดู", - "session.share.copy.copied": "คัดลอกแล้ว", - "session.share.copy.copyLink": "คัดลอกลิงก์", - - "lsp.tooltip.none": "ไม่มีเซิร์ฟเวอร์ LSP", - "lsp.label.connected": "{{count}} LSP", - - "prompt.loading": "กำลังโหลดพร้อมท์...", - "terminal.loading": "กำลังโหลดเทอร์มินัล...", - "terminal.title": "เทอร์มินัล", - "terminal.title.numbered": "เทอร์มินัล {{number}}", - "terminal.close": "ปิดเทอร์มินัล", - "terminal.connectionLost.title": "การเชื่อมต่อขาดหาย", - "terminal.connectionLost.description": "การเชื่อมต่อเทอร์มินัลถูกขัดจังหวะ อาจเกิดขึ้นเมื่อเซิร์ฟเวอร์รีสตาร์ท", - - "common.closeTab": "ปิดแท็บ", - "common.dismiss": "ปิด", - "common.requestFailed": "คำขอล้มเหลว", - "common.moreOptions": "ตัวเลือกเพิ่มเติม", - "common.learnMore": "เรียนรู้เพิ่มเติม", - "common.rename": "เปลี่ยนชื่อ", - "common.reset": "รีเซ็ต", - "common.archive": "จัดเก็บ", - "common.delete": "ลบ", - "common.close": "ปิด", - "common.edit": "แก้ไข", - "common.loadMore": "โหลดเพิ่มเติม", - "common.key.esc": "ESC", - - "sidebar.menu.toggle": "สลับเมนู", - "sidebar.nav.projectsAndSessions": "โปรเจกต์และเซสชัน", - "sidebar.settings": "การตั้งค่า", - "sidebar.help": "ช่วยเหลือ", - "sidebar.workspaces.enable": "เปิดใช้งานพื้นที่ทำงาน", - "sidebar.workspaces.disable": "ปิดใช้งานพื้นที่ทำงาน", - "sidebar.gettingStarted.title": "เริ่มต้นใช้งาน", - "sidebar.gettingStarted.line1": "Kilo รวมถึงโมเดลฟรีเพื่อให้คุณเริ่มต้นได้ทันที", - "sidebar.gettingStarted.line2": "เชื่อมต่อผู้ให้บริการใด ๆ เพื่อใช้โมเดล รวมถึง Claude, GPT, Gemini ฯลฯ", - "sidebar.project.recentSessions": "เซสชันล่าสุด", - "sidebar.project.viewAllSessions": "ดูเซสชันทั้งหมด", - "sidebar.project.clearNotifications": "ล้างการแจ้งเตือน", - - "app.name.desktop": "Kilo Desktop", - - "settings.section.desktop": "เดสก์ท็อป", - "settings.section.server": "เซิร์ฟเวอร์", - "settings.tab.general": "ทั่วไป", - "settings.tab.shortcuts": "ทางลัด", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "การรวม WSL", - "settings.desktop.wsl.description": "เรียกใช้เซิร์ฟเวอร์ Kilo ภายใน WSL บน Windows", - - "settings.general.section.appearance": "รูปลักษณ์", - "settings.general.section.notifications": "การแจ้งเตือนระบบ", - "settings.general.section.updates": "การอัปเดต", - "settings.general.section.sounds": "เสียงเอฟเฟกต์", - "settings.general.section.feed": "ฟีด", - "settings.general.section.display": "การแสดงผล", - - "settings.general.row.language.title": "ภาษา", - "settings.general.row.language.description": "เปลี่ยนภาษาที่แสดงสำหรับ Kilo", - "settings.general.row.appearance.title": "รูปลักษณ์", - "settings.general.row.appearance.description": "ปรับแต่งวิธีการที่ Kilo มีลักษณะบนอุปกรณ์ของคุณ", - "settings.general.row.colorScheme.title": "โทนสี", - "settings.general.row.colorScheme.description": "เลือกว่าจะให้ Kilo ใช้ธีมตามระบบ สว่าง หรือมืด", - "settings.general.row.theme.title": "ธีม", - "settings.general.row.theme.description": "ปรับแต่งวิธีการที่ Kilo มีธีม", - "settings.general.row.font.title": "ฟอนต์โค้ด", - "settings.general.row.font.description": "ปรับแต่งฟอนต์ที่ใช้ในบล็อกโค้ด", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "ฟอนต์ UI", - "settings.general.row.uiFont.description": "ปรับแต่งฟอนต์ที่ใช้ทั่วทั้งอินเทอร์เฟซ", - "settings.general.row.followup.title": "พฤติกรรมการติดตามผล", - "settings.general.row.followup.description": "เลือกว่าจะให้พร้อมท์ติดตามผลทำงานทันทีหรือรอในคิว", - "settings.general.row.followup.option.queue": "คิว", - "settings.general.row.followup.option.steer": "นำทาง", - "settings.general.row.reasoningSummaries.title": "แสดงสรุปการใช้เหตุผล", - "settings.general.row.reasoningSummaries.description": "แสดงสรุปการใช้เหตุผลของโมเดลในไทม์ไลน์", - "settings.general.row.shellToolPartsExpanded.title": "ขยายส่วนเครื่องมือ shell", - "settings.general.row.shellToolPartsExpanded.description": "แสดงส่วนเครื่องมือ shell แบบขยายตามค่าเริ่มต้นในไทม์ไลน์", - "settings.general.row.editToolPartsExpanded.title": "ขยายส่วนเครื่องมือ edit", - "settings.general.row.editToolPartsExpanded.description": - "แสดงส่วนเครื่องมือ edit, write และ patch แบบขยายตามค่าเริ่มต้นในไทม์ไลน์", - "settings.general.row.showSessionProgressBar.title": "แสดงแถบความคืบหน้าของเซสชัน", - "settings.general.row.showSessionProgressBar.description": - "แสดงแถบความคืบหน้าแบบเคลื่อนไหวที่ด้านบนของเซสชันเมื่อเอเจนต์กำลังทำงาน", - "settings.general.row.wayland.title": "ใช้ Wayland แบบเนทีฟ", - "settings.general.row.wayland.description": "ปิดใช้งาน X11 fallback บน Wayland ต้องรีสตาร์ท", - "settings.general.row.wayland.tooltip": "บน Linux ที่มีจอภาพรีเฟรชเรตแบบผสม Wayland แบบเนทีฟอาจเสถียรกว่า", - - "settings.general.row.releaseNotes.title": "บันทึกการอัปเดต", - "settings.general.row.releaseNotes.description": "แสดงป๊อปอัพ What's New หลังจากอัปเดต", - - "settings.updates.row.startup.title": "ตรวจสอบการอัปเดตเมื่อเริ่มต้น", - "settings.updates.row.startup.description": "ตรวจสอบการอัปเดตโดยอัตโนมัติเมื่อ Kilo เปิดใช้งาน", - "settings.updates.row.check.title": "ตรวจสอบการอัปเดต", - "settings.updates.row.check.description": "ตรวจสอบการอัปเดตด้วยตนเองและติดตั้งหากมี", - "settings.updates.action.checkNow": "ตรวจสอบทันที", - "settings.updates.action.checking": "กำลังตรวจสอบ...", - "settings.updates.toast.latest.title": "คุณเป็นเวอร์ชันล่าสุดแล้ว", - "settings.updates.toast.latest.description": "คุณกำลังใช้งาน Kilo เวอร์ชันล่าสุด", - - "sound.option.none": "ไม่มี", - "sound.option.alert01": "เสียงเตือน 01", - "sound.option.alert02": "เสียงเตือน 02", - "sound.option.alert03": "เสียงเตือน 03", - "sound.option.alert04": "เสียงเตือน 04", - "sound.option.alert05": "เสียงเตือน 05", - "sound.option.alert06": "เสียงเตือน 06", - "sound.option.alert07": "เสียงเตือน 07", - "sound.option.alert08": "เสียงเตือน 08", - "sound.option.alert09": "เสียงเตือน 09", - "sound.option.alert10": "เสียงเตือน 10", - "sound.option.bipbop01": "Bip-bop 01", - "sound.option.bipbop02": "Bip-bop 02", - "sound.option.bipbop03": "Bip-bop 03", - "sound.option.bipbop04": "Bip-bop 04", - "sound.option.bipbop05": "Bip-bop 05", - "sound.option.bipbop06": "Bip-bop 06", - "sound.option.bipbop07": "Bip-bop 07", - "sound.option.bipbop08": "Bip-bop 08", - "sound.option.bipbop09": "Bip-bop 09", - "sound.option.bipbop10": "Bip-bop 10", - "sound.option.staplebops01": "Staplebops 01", - "sound.option.staplebops02": "Staplebops 02", - "sound.option.staplebops03": "Staplebops 03", - "sound.option.staplebops04": "Staplebops 04", - "sound.option.staplebops05": "Staplebops 05", - "sound.option.staplebops06": "Staplebops 06", - "sound.option.staplebops07": "Staplebops 07", - "sound.option.nope01": "Nope 01", - "sound.option.nope02": "Nope 02", - "sound.option.nope03": "Nope 03", - "sound.option.nope04": "Nope 04", - "sound.option.nope05": "Nope 05", - "sound.option.nope06": "Nope 06", - "sound.option.nope07": "Nope 07", - "sound.option.nope08": "Nope 08", - "sound.option.nope09": "Nope 09", - "sound.option.nope10": "Nope 10", - "sound.option.nope11": "Nope 11", - "sound.option.nope12": "Nope 12", - "sound.option.yup01": "Yup 01", - "sound.option.yup02": "Yup 02", - "sound.option.yup03": "Yup 03", - "sound.option.yup04": "Yup 04", - "sound.option.yup05": "Yup 05", - "sound.option.yup06": "Yup 06", - - "settings.general.notifications.agent.title": "เอเจนต์", - "settings.general.notifications.agent.description": "แสดงการแจ้งเตือนระบบเมื่อเอเจนต์เสร็จสิ้นหรือต้องการความสนใจ", - "settings.general.notifications.permissions.title": "สิทธิ์", - "settings.general.notifications.permissions.description": "แสดงการแจ้งเตือนระบบเมื่อต้องการสิทธิ์", - "settings.general.notifications.errors.title": "ข้อผิดพลาด", - "settings.general.notifications.errors.description": "แสดงการแจ้งเตือนระบบเมื่อเกิดข้อผิดพลาด", - - "settings.general.sounds.agent.title": "เอเจนต์", - "settings.general.sounds.agent.description": "เล่นเสียงเมื่อเอเจนต์เสร็จสิ้นหรือต้องการความสนใจ", - "settings.general.sounds.permissions.title": "สิทธิ์", - "settings.general.sounds.permissions.description": "เล่นเสียงเมื่อต้องการสิทธิ์", - "settings.general.sounds.errors.title": "ข้อผิดพลาด", - "settings.general.sounds.errors.description": "เล่นเสียงเมื่อเกิดข้อผิดพลาด", - - "settings.shortcuts.title": "ทางลัดแป้นพิมพ์", - "settings.shortcuts.reset.button": "รีเซ็ตเป็นค่าเริ่มต้น", - "settings.shortcuts.reset.toast.title": "รีเซ็ตทางลัดแล้ว", - "settings.shortcuts.reset.toast.description": "รีเซ็ตทางลัดแป้นพิมพ์เป็นค่าเริ่มต้นแล้ว", - "settings.shortcuts.conflict.title": "ทางลัดใช้งานอยู่แล้ว", - "settings.shortcuts.conflict.description": "{{keybind}} ถูกกำหนดให้กับ {{titles}} แล้ว", - "settings.shortcuts.unassigned": "ไม่ได้กำหนด", - "settings.shortcuts.pressKeys": "กดปุ่ม", - "settings.shortcuts.search.placeholder": "ค้นหาทางลัด", - "settings.shortcuts.search.empty": "ไม่พบทางลัด", - - "settings.shortcuts.group.general": "ทั่วไป", - "settings.shortcuts.group.session": "เซสชัน", - "settings.shortcuts.group.navigation": "การนำทาง", - "settings.shortcuts.group.modelAndAgent": "โมเดลและเอเจนต์", - "settings.shortcuts.group.terminal": "เทอร์มินัล", - "settings.shortcuts.group.prompt": "พร้อมท์", - - "settings.providers.title": "ผู้ให้บริการ", - "settings.providers.description": "การตั้งค่าผู้ให้บริการจะสามารถกำหนดค่าได้ที่นี่", - "settings.providers.section.connected": "ผู้ให้บริการที่เชื่อมต่อ", - "settings.providers.connected.empty": "ไม่มีผู้ให้บริการที่เชื่อมต่อ", - "settings.providers.section.popular": "ผู้ให้บริการยอดนิยม", - "settings.providers.tag.environment": "สภาพแวดล้อม", - "settings.providers.tag.config": "กำหนดค่า", - "settings.providers.tag.custom": "กำหนดเอง", - "settings.providers.tag.other": "อื่น ๆ", - "settings.models.title": "โมเดล", - "settings.models.description": "การตั้งค่าโมเดลจะสามารถกำหนดค่าได้ที่นี่", - "settings.agents.title": "เอเจนต์", - "settings.agents.description": "การตั้งค่าเอเจนต์จะสามารถกำหนดค่าได้ที่นี่", - "settings.commands.title": "คำสั่ง", - "settings.commands.description": "การตั้งค่าคำสั่งจะสามารถกำหนดค่าได้ที่นี่", - "settings.mcp.title": "MCP", - "settings.mcp.description": "การตั้งค่า MCP จะสามารถกำหนดค่าได้ที่นี่", - - "settings.permissions.title": "สิทธิ์", - "settings.permissions.description": "ควบคุมเครื่องมือที่เซิร์ฟเวอร์สามารถใช้โดยค่าเริ่มต้น", - "settings.permissions.section.tools": "เครื่องมือ", - "settings.permissions.toast.updateFailed.title": "ไม่สามารถอัปเดตสิทธิ์", - - "settings.permissions.action.allow": "อนุญาต", - "settings.permissions.action.ask": "ถาม", - "settings.permissions.action.deny": "ปฏิเสธ", - - "settings.permissions.tool.read.title": "อ่าน", - "settings.permissions.tool.read.description": "อ่านไฟล์ (ตรงกับเส้นทางไฟล์)", - "settings.permissions.tool.edit.title": "แก้ไข", - "settings.permissions.tool.edit.description": "แก้ไขไฟล์ รวมถึงการแก้ไข เขียน แพตช์ และแก้ไขหลายรายการ", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "จับคู่ไฟล์โดยใช้รูปแบบ glob", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "ค้นหาเนื้อหาไฟล์โดยใช้นิพจน์ทั่วไป", - "settings.permissions.tool.list.title": "รายการ", - "settings.permissions.tool.list.description": "แสดงรายการไฟล์ภายในไดเรกทอรี", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "เรียกใช้คำสั่งเชลล์", - "settings.permissions.tool.task.title": "งาน", - "settings.permissions.tool.task.description": "เปิดเอเจนต์ย่อย", - "settings.permissions.tool.skill.title": "ทักษะ", - "settings.permissions.tool.skill.description": "โหลดทักษะตามชื่อ", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "เรียกใช้การสืบค้นเซิร์ฟเวอร์ภาษา", - "settings.permissions.tool.todowrite.title": "เขียนรายการงาน", - "settings.permissions.tool.todowrite.description": "อัปเดตรายการงาน", - "settings.permissions.tool.webfetch.title": "ดึงข้อมูลจากเว็บ", - "settings.permissions.tool.webfetch.description": "ดึงเนื้อหาจาก URL", - "settings.permissions.tool.websearch.title": "ค้นหาเว็บ", - "settings.permissions.tool.websearch.description": "ค้นหาบนเว็บ", - "settings.permissions.tool.codesearch.title": "ค้นหาโค้ด", - "settings.permissions.tool.codesearch.description": "ค้นหาโค้ดบนเว็บ", - "settings.permissions.tool.external_directory.title": "ไดเรกทอรีภายนอก", - "settings.permissions.tool.external_directory.description": "เข้าถึงไฟล์นอกไดเรกทอรีโปรเจกต์", - "settings.permissions.tool.doom_loop.title": "Doom Loop", - "settings.permissions.tool.doom_loop.description": "ตรวจจับการเรียกเครื่องมือซ้ำด้วยข้อมูลนำเข้าเหมือนกัน", - - "session.delete.failed.title": "ไม่สามารถลบเซสชัน", - "session.delete.title": "ลบเซสชัน", - "session.delete.confirm": 'ลบเซสชัน "{{name}}" หรือไม่?', - "session.delete.button": "ลบเซสชัน", - - "workspace.new": "พื้นที่ทำงานใหม่", - "workspace.type.local": "ในเครื่อง", - "workspace.type.sandbox": "แซนด์บ็อกซ์", - "workspace.create.failed.title": "ไม่สามารถสร้างพื้นที่ทำงาน", - "workspace.delete.failed.title": "ไม่สามารถลบพื้นที่ทำงาน", - "workspace.resetting.title": "กำลังรีเซ็ตพื้นที่ทำงาน", - "workspace.resetting.description": "อาจใช้เวลาประมาณหนึ่งนาที", - "workspace.reset.failed.title": "ไม่สามารถรีเซ็ตพื้นที่ทำงาน", - "workspace.reset.success.title": "รีเซ็ตพื้นที่ทำงานแล้ว", - "workspace.reset.success.description": "พื้นที่ทำงานตรงกับสาขาเริ่มต้นแล้ว", - "workspace.error.stillPreparing": "พื้นที่ทำงานกำลังเตรียมอยู่", - "workspace.status.checking": "กำลังตรวจสอบการเปลี่ยนแปลงที่ไม่ได้ผสาน...", - "workspace.status.error": "ไม่สามารถตรวจสอบสถานะ git", - "workspace.status.clean": "ไม่ตรวจพบการเปลี่ยนแปลงที่ไม่ได้ผสาน", - "workspace.status.dirty": "ตรวจพบการเปลี่ยนแปลงที่ไม่ได้ผสานในพื้นที่ทำงานนี้", - "workspace.delete.title": "ลบพื้นที่ทำงาน", - "workspace.delete.confirm": 'ลบพื้นที่ทำงาน "{{name}}" หรือไม่?', - "workspace.delete.button": "ลบพื้นที่ทำงาน", - "workspace.reset.title": "รีเซ็ตพื้นที่ทำงาน", - "workspace.reset.confirm": 'รีเซ็ตพื้นที่ทำงาน "{{name}}" หรือไม่?', - "workspace.reset.button": "รีเซ็ตพื้นที่ทำงาน", - "workspace.reset.archived.none": "ไม่มีเซสชันที่ใช้งานอยู่จะถูกจัดเก็บ", - "workspace.reset.archived.one": "1 เซสชันจะถูกจัดเก็บ", - "workspace.reset.archived.many": "{{count}} เซสชันจะถูกจัดเก็บ", - "workspace.reset.note": "สิ่งนี้จะรีเซ็ตพื้นที่ทำงานให้ตรงกับสาขาเริ่มต้น", - "common.open": "เปิด", - "dialog.releaseNotes.action.getStarted": "เริ่มต้น", - "dialog.releaseNotes.action.next": "ถัดไป", - "dialog.releaseNotes.action.hideFuture": "ไม่ต้องแสดงสิ่งนี้อีกในอนาคต", - "dialog.releaseNotes.media.alt": "ตัวอย่างรุ่น", - "toast.project.reloadFailed.title": "ไม่สามารถโหลด {{project}} ใหม่ได้", - "error.server.invalidConfiguration": "การกำหนดค่าไม่ถูกต้อง", - "common.moreCountSuffix": " (เพิ่มอีก {{count}})", - "common.time.justNow": "เมื่อสักครู่นี้", - "common.time.minutesAgo.short": "{{count}} นาทีที่แล้ว", - "common.time.hoursAgo.short": "{{count}} ชม. ที่แล้ว", - "common.time.daysAgo.short": "{{count}} วันที่แล้ว", - "settings.providers.connected.environmentDescription": "เชื่อมต่อจากตัวแปรสภาพแวดล้อมของคุณ", - "settings.providers.custom.description": "เพิ่มผู้ให้บริการที่รองรับ OpenAI ด้วย URL หลัก", - - "app.server.unreachable": "ไม่สามารถติดต่อ {{server}}", - "app.server.retrying": "กำลังลองใหม่โดยอัตโนมัติ...", - "app.server.otherServers": "เซิร์ฟเวอร์อื่น ๆ", - "dialog.server.add.usernamePlaceholder": "ชื่อผู้ใช้", - "dialog.server.add.passwordPlaceholder": "รหัสผ่าน", - "server.row.noUsername": "ไม่มีชื่อผู้ใช้", - "session.review.noVcs.createGit.title": "สร้าง Git repository", - "session.review.noVcs.createGit.description": "ติดตาม ตรวจสอบ และเลิกทำสิ่งเปลี่ยนแปลงในโปรเจกต์นี้", - "session.review.noVcs.createGit.actionLoading": "กำลังสร้าง Git repository...", - "session.review.noVcs.createGit.action": "สร้าง Git repository", - "session.todo.progress": "เสร็จสิ้น {{done}} จาก {{total}} รายการ", - "session.question.progress": "{{current}} จาก {{total}} คำถาม", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "File Explorer", - "session.header.open.fileManager": "File Manager", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "Terminal", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "การวินิจฉัยประสิทธิภาพการพัฒนา", - "debugBar.na": "n/a", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": - "การเปลี่ยนเส้นทางที่เสร็จสมบูรณ์ล่าสุดที่สัมผัสหน้าเซสชัน วัดจากจุดเริ่มต้นเราเตอร์จนถึงการวาดครั้งแรกหลังจากที่นิ่ง", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "เฟรมต่อวินาทีแบบต่อเนื่องในช่วง 5 วินาทีที่ผ่านมา", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "เวลาเฟรมที่แย่ที่สุดในช่วง 5 วินาทีที่ผ่านมา", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "เฟรมที่เกิน 32ms ในช่วง 5 วินาทีที่ผ่านมา", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "เวลาที่ถูกบล็อกและจำนวนงานยาวในช่วง 5 วินาทีที่ผ่านมา งานสูงสุด: {{max}}", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "ความล่าช้าในการป้อนข้อมูลที่แย่ที่สุดที่สังเกตได้ในช่วง 5 วินาทีที่ผ่านมา", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": - "ระยะเวลาการโต้ตอบโดยประมาณในช่วง 5 วินาทีที่ผ่านมา นี่เป็นเหมือน INP ไม่ใช่ Web Vitals INP อย่างเป็นทางการ", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "การเลื่อนเลย์เอาต์สะสมสำหรับอายุการใช้งานของแอปปัจจุบัน", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "JS heap ที่ใช้เทียบกับขีดจำกัด heap เฉพาะ Chromium", - "debugBar.mem.tip": "JS heap ที่ใช้เทียบกับขีดจำกัด heap {{used}} จาก {{limit}}", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "Space", - "common.key.backspace": "Backspace", - "common.key.enter": "Enter", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "ไม่ทราบ", - "error.page.circular": "[วงกลม]", - "error.globalSDK.noServerAvailable": "ไม่มีเซิร์ฟเวอร์", - "error.globalSDK.serverNotAvailable": "เซิร์ฟเวอร์ไม่พร้อมใช้งาน", - "error.childStore.persistedCacheCreateFailed": "ไม่สามารถสร้างแคชถาวร", - "error.childStore.persistedProjectMetadataCreateFailed": "ไม่สามารถสร้างเมตาดาต้าโปรเจกต์ถาวร", - "error.childStore.persistedProjectIconCreateFailed": "ไม่สามารถสร้างไอคอนโปรเจกต์ถาวร", - "error.childStore.storeCreateFailed": "ไม่สามารถสร้างที่เก็บ", - "terminal.connectionLost.abnormalClose": "WebSocket ปิดอย่างผิดปกติ: {{code}}", -} diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts deleted file mode 100644 index a5c3f4b47b5..00000000000 --- a/packages/app/src/i18n/tr.ts +++ /dev/null @@ -1,942 +0,0 @@ -import { dict as en } from "./en" - -type Keys = keyof typeof en - -export const dict = { - "command.category.suggested": "Önerilen", - "command.category.view": "Görünüm", - "command.category.project": "Proje", - "command.category.provider": "Sağlayıcı", - "command.category.server": "Sunucu", - "command.category.session": "Oturum", - "command.category.theme": "Tema", - "command.category.language": "Dil", - "command.category.file": "Dosya", - "command.category.context": "Bağlam", - "command.category.terminal": "Terminal", - "command.category.model": "Model", - "command.category.mcp": "MCP", - "command.category.agent": "Ajan", - "command.category.permissions": "İzinler", - "command.category.workspace": "Çalışma Alanı", - "command.category.settings": "Ayarlar", - - "theme.scheme.system": "Sistem", - "theme.scheme.light": "Açık", - "theme.scheme.dark": "Koyu", - - "command.sidebar.toggle": "Kenar çubuğunu aç/kapat", - "command.project.open": "Proje aç", - "command.provider.connect": "Sağlayıcı bağla", - "command.server.switch": "Sunucu değiştir", - "command.settings.open": "Ayarları aç", - "command.session.previous": "Önceki oturum", - "command.session.next": "Sonraki oturum", - "command.session.previous.unseen": "Önceki okunmamış oturum", - "command.session.next.unseen": "Sonraki okunmamış oturum", - "command.session.archive": "Oturumu arşivle", - - "command.palette": "Komut paleti", - - "command.theme.cycle": "Tema değiştir", - "command.theme.set": "Tema kullan: {{theme}}", - "command.theme.scheme.cycle": "Renk şemasını değiştir", - "command.theme.scheme.set": "Renk şeması kullan: {{scheme}}", - - "command.language.cycle": "Dil değiştir", - "command.language.set": "Dil kullan: {{language}}", - - "command.session.new": "Yeni oturum", - "command.file.open": "Dosya aç", - "command.tab.close": "Sekmeyi kapat", - "command.context.addSelection": "Seçimi bağlama ekle", - "command.context.addSelection.description": "Mevcut dosyadan seçili satırları ekle", - "command.input.focus": "Girişi odakla", - "command.terminal.toggle": "Terminali aç/kapat", - "command.fileTree.toggle": "Dosya ağacını aç/kapat", - "command.review.toggle": "İncelemeyi aç/kapat", - "command.terminal.new": "Yeni terminal", - "command.terminal.new.description": "Yeni bir terminal sekmesi oluştur", - "command.steps.toggle": "Adımları aç/kapat", - "command.steps.toggle.description": "Mevcut mesaj için adımları göster veya gizle", - "command.message.previous": "Önceki mesaj", - "command.message.previous.description": "Önceki kullanıcı mesajına git", - "command.message.next": "Sonraki mesaj", - "command.message.next.description": "Sonraki kullanıcı mesajına git", - "command.model.choose": "Model seç", - "command.model.choose.description": "Farklı bir model seç", - "command.mcp.toggle": "MCP'leri aç/kapat", - "command.mcp.toggle.description": "MCP'leri aç/kapat", - "command.agent.cycle": "Ajan değiştir", - "command.agent.cycle.description": "Sonraki ajana geç", - "command.agent.cycle.reverse": "Ajanı geri değiştir", - "command.agent.cycle.reverse.description": "Önceki ajana geç", - "command.model.variant.cycle": "Düşünme eforu değiştir", - "command.model.variant.cycle.description": "Sonraki efor seviyesine geç", - "command.prompt.mode.shell": "Kabuk", - "command.prompt.mode.normal": "Komut", - "command.permissions.autoaccept.enable": "Düzenlemeleri otomatik kabul et", - "command.permissions.autoaccept.disable": "Otomatik kabulü durdur", - "command.workspace.toggle": "Çalışma alanlarını aç/kapat", - "command.workspace.toggle.description": "Kenar çubuğunda birden fazla çalışma alanını göster veya gizle", - "command.session.undo": "Geri al", - "command.session.undo.description": "Son mesajı geri al", - "command.session.redo": "Yinele", - "command.session.redo.description": "Son geri alınan mesajı yinele", - "command.session.compact": "Oturumu sıkıştır", - "command.session.compact.description": "Bağlam boyutunu azaltmak için oturumu özetle", - "command.session.fork": "Mesajdan dallandır", - "command.session.fork.description": "Önceki bir mesajdan yeni oturum oluştur", - "command.session.share": "Oturumu paylaş", - "command.session.share.description": "Bu oturumu paylaş ve URL'yi panoya kopyala", - "command.session.unshare": "Paylaşımı kaldır", - "command.session.unshare.description": "Bu oturumun paylaşımını durdur", - - "palette.search.placeholder": "Dosya, komut ve oturum ara", - "palette.empty": "Sonuç bulunamadı", - "palette.group.commands": "Komutlar", - "palette.group.files": "Dosyalar", - - "dialog.provider.search.placeholder": "Sağlayıcı ara", - "dialog.provider.empty": "Sağlayıcı bulunamadı", - "dialog.provider.group.popular": "Popüler", - "dialog.provider.group.other": "Diğer", - "dialog.provider.tag.recommended": "Önerilen", - "dialog.provider.opencode.note": "Claude, GPT, Gemini ve daha fazlasını içeren seçilmiş modeller", - "dialog.provider.opencode.tagline": "Güvenilir optimize edilmiş modeller", - "dialog.provider.opencodeGo.tagline": "Herkes için düşük maliyetli abonelik", - "dialog.provider.anthropic.note": "Pro ve Max dahil Claude modellerine doğrudan erişim", - "dialog.provider.copilot.note": "GitHub Copilot üzerinden kodlama yardımı için yapay zekâ modelleri", - "dialog.provider.openai.note": "Hızlı ve yetenekli genel yapay zekâ görevleri için GPT modelleri", - "dialog.provider.google.note": "Hızlı ve yapılandırılmış yanıtlar için Gemini modelleri", - "dialog.provider.openrouter.note": "Tek bir sağlayıcıdan tüm desteklenen modellere eriş", - "dialog.provider.vercel.note": "Akıllı yönlendirme ile yapay zekâ modellerine birleşik erişim", - - "dialog.model.select.title": "Model seç", - "dialog.model.search.placeholder": "Model ara", - "dialog.model.empty": "Model sonucu yok", - "dialog.model.manage": "Modelleri yönet", - "dialog.model.manage.description": "Model seçicide hangi modellerin görüneceğini özelleştirin.", - "dialog.model.manage.provider.toggle": "Tüm {{provider}} modellerini aç/kapat", - - "dialog.model.unpaid.freeModels.title": "Kilo tarafından sunulan ücretsiz modeller", - "dialog.model.unpaid.addMore.title": "Popüler sağlayıcılardan daha fazla model ekleyin", - - "dialog.provider.viewAll": "Daha fazla sağlayıcı göster", - - "provider.connect.title": "{{provider}} bağla", - "provider.connect.title.anthropicProMax": "Claude Pro/Max ile giriş yap", - "provider.connect.selectMethod": "{{provider}} için giriş yöntemini seçin.", - "provider.connect.method.apiKey": "API anahtarı", - "provider.connect.status.inProgress": "Yetkilendirme devam ediyor...", - "provider.connect.status.waiting": "Yetkilendirme bekleniyor...", - "provider.connect.status.failed": "Yetkilendirme başarısız: {{error}}", - "provider.connect.apiKey.description": - "{{provider}} hesabınızı bağlamak ve Kilo'da {{provider}} modellerini kullanmak için {{provider}} API anahtarınızı girin.", - "provider.connect.apiKey.label": "{{provider}} API anahtarı", - "provider.connect.apiKey.placeholder": "API anahtarı", - "provider.connect.apiKey.required": "API anahtarı gerekli", - "provider.connect.opencodeZen.line1": - "OpenCode Zen, kodlama ajanları için seçilmiş güvenilir optimize edilmiş modellere erişim sağlar.", - "provider.connect.opencodeZen.line2": - "Tek bir API anahtarıyla Claude, GPT, Gemini, GLM ve daha fazlası gibi modellere erişebilirsiniz.", - "provider.connect.opencodeZen.visit.prefix": "", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " adresini ziyaret ederek API anahtarınızı alın.", - "provider.connect.oauth.code.visit.prefix": "Hesabınızı bağlamak ve Kilo'da {{provider}} modellerini kullanmak için ", - "provider.connect.oauth.code.visit.link": "bu bağlantıya", - "provider.connect.oauth.code.visit.suffix": " tıklayarak yetkilendirme kodunuzu alın.", - "provider.connect.oauth.code.label": "{{method}} yetkilendirme kodu", - "provider.connect.oauth.code.placeholder": "Yetkilendirme kodu", - "provider.connect.oauth.code.required": "Yetkilendirme kodu gerekli", - "provider.connect.oauth.code.invalid": "Geçersiz yetkilendirme kodu", - "provider.connect.oauth.auto.visit.prefix": "", - "provider.connect.oauth.auto.visit.link": "Bu bağlantıya", - "provider.connect.oauth.auto.visit.suffix": - " tıklayarak aşağıdaki kodu girin ve hesabınızı bağlayarak Kilo'da {{provider}} modellerini kullanın.", - "provider.connect.oauth.auto.confirmationCode": "Onay kodu", - "provider.connect.toast.connected.title": "{{provider}} bağlandı", - "provider.connect.toast.connected.description": "{{provider}} modelleri artık kullanımda.", - - "provider.custom.title": "Özel sağlayıcı", - "provider.custom.description.prefix": "OpenAI uyumlu bir sağlayıcı yapılandırın. ", - "provider.custom.description.link": "Sağlayıcı yapılandırma dökümanları", - "provider.custom.description.suffix": " sayfasına bakın.", - "provider.custom.field.providerID.label": "Sağlayıcı kimlik", - "provider.custom.field.providerID.placeholder": "saglayicim", - "provider.custom.field.providerID.description": "Küçük harfler, rakamlar, tire veya alt çizgi", - "provider.custom.field.name.label": "Görünen ad", - "provider.custom.field.name.placeholder": "Yapay Zekâ Sağlayıcım", - "provider.custom.field.baseURL.label": "Temel URL", - "provider.custom.field.baseURL.placeholder": "https://api.saglayicim.com/v1", - "provider.custom.field.apiKey.label": "API anahtarı", - "provider.custom.field.apiKey.placeholder": "API anahtarı", - "provider.custom.field.apiKey.description": - "İsteğe bağlı. Kimlik doğrulamayı başlıklar ile yönetiyorsanız boş bırakın.", - "provider.custom.models.label": "Modeller", - "provider.custom.models.id.label": "Kimlik", - "provider.custom.models.id.placeholder": "model-kimlik", - "provider.custom.models.name.label": "Ad", - "provider.custom.models.name.placeholder": "Görünen Ad", - "provider.custom.models.remove": "Modeli kaldır", - "provider.custom.models.add": "Model ekle", - "provider.custom.headers.label": "Başlıklar (isteğe bağlı)", - "provider.custom.headers.key.label": "Başlık", - "provider.custom.headers.key.placeholder": "Başlık-Adı", - "provider.custom.headers.value.label": "Değer", - "provider.custom.headers.value.placeholder": "değer", - "provider.custom.headers.remove": "Başlığı kaldır", - "provider.custom.headers.add": "Başlık ekle", - "provider.custom.error.providerID.required": "Sağlayıcı kimlik gerekli", - "provider.custom.error.providerID.format": "Küçük harf, rakam, tire veya alt çizgi kullanın", - "provider.custom.error.providerID.exists": "Bu sağlayıcı kimlik zaten mevcut", - "provider.custom.error.name.required": "Görünen ad gerekli", - "provider.custom.error.baseURL.required": "Temel URL gerekli", - "provider.custom.error.baseURL.format": "http:// veya https:// ile başlamalı", - "provider.custom.error.required": "Gerekli", - "provider.custom.error.duplicate": "Tekrar", - - "provider.disconnect.toast.disconnected.title": "{{provider}} bağlantısı kesildi", - "provider.disconnect.toast.disconnected.description": "{{provider}} modelleri artık kullanılabilir değil.", - - "model.tag.free": "Ücretsiz", - "model.tag.latest": "En yeni", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "metin", - "model.input.image": "görsel", - "model.input.audio": "ses", - "model.input.video": "video", - "model.input.pdf": "pdf", - "model.tooltip.allows": "Kabul eder: {{inputs}}", - "model.tooltip.reasoning.allowed": "Akıl yürütme destekler", - "model.tooltip.reasoning.none": "Akıl yürütme yok", - "model.tooltip.context": "Bağlam limiti {{limit}}", - - "common.search.placeholder": "Ara", - "common.goBack": "Geri git", - "common.goForward": "İleri git", - "common.loading": "Yükleniyor", - "common.loading.ellipsis": "...", - "common.cancel": "İptal", - "common.connect": "Bağlan", - "common.disconnect": "Bağlantı Kes", - "common.continue": "Gönder", - "common.submit": "Gönder", - "common.save": "Kaydet", - "common.saving": "Kaydediliyor...", - "common.default": "Varsayılan", - "common.attachment": "ek", - - "prompt.placeholder.shell": "Kabuk komutu girin... {{example}}", - "prompt.placeholder.normal": 'Bir şeyler sorun... "{{example}}"', - "prompt.placeholder.simple": "Bir şeyler sorun...", - "prompt.placeholder.summarizeComments": "Yorumları özetle…", - "prompt.placeholder.summarizeComment": "Yorumu özetle…", - "prompt.mode.shell": "Kabuk", - "prompt.mode.normal": "Komut", - "prompt.mode.shell.exit": "çıkmak için esc", - - "prompt.example.1": "Kod tabanındaki bir TODO'yu düzelt", - "prompt.example.2": "Bu projenin teknoloji yığını nedir?", - "prompt.example.3": "Bozuk testleri düzelt", - "prompt.example.4": "Kimlik doğrulamanın nasıl çalıştığını açıkla", - "prompt.example.5": "Güvenlik açıkları bul ve düzelt", - "prompt.example.6": "Kullanıcı servisi için birim testleri ekle", - "prompt.example.7": "Bu fonksiyonu daha okunabilir hale getir", - "prompt.example.8": "Bu hata ne anlama geliyor?", - "prompt.example.9": "Bu sorunu ayıklamama yardım et", - "prompt.example.10": "API dokümantasyonu oluştur", - "prompt.example.11": "Veritabanı sorgularını optimize et", - "prompt.example.12": "Girdi doğrulama ekle", - "prompt.example.13": "İçin yeni bir bileşen oluştur...", - "prompt.example.14": "Bu projeyi nasıl dağıtabilirim?", - "prompt.example.15": "Kodumu en iyi uygulamalar için incele", - "prompt.example.16": "Bu fonksiyona hata yönetimi ekle", - "prompt.example.17": "Bu regex kalıbını açıkla", - "prompt.example.18": "Bunu TypeScript'e dönüştür", - "prompt.example.19": "Kod tabanı boyunca loglama ekle", - "prompt.example.20": "Hangi bağımlılıklar güncellenmemiş?", - "prompt.example.21": "Bir göç betiği yazmama yardım et", - "prompt.example.22": "Bu uç nokta için önbellekleme uygula", - "prompt.example.23": "Bu listeye sayfalama ekle", - "prompt.example.24": "İçin bir CLI komutu oluştur...", - "prompt.example.25": "Ortam değişkenleri burada nasıl çalışıyor?", - - "prompt.popover.emptyResults": "Eşleşen sonuç yok", - "prompt.popover.emptyCommands": "Eşleşen komut yok", - "prompt.dropzone.label": "Resimleri, PDF'leri veya metin dosyalarını buraya bırakın", - "prompt.dropzone.file.label": "@bahsetmek için dosyayı bırakın", - "prompt.slash.badge.custom": "özel", - "prompt.slash.badge.skill": "beceri", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "aktif", - "prompt.context.includeActiveFile": "Aktif dosyayı dahil et", - "prompt.context.removeActiveFile": "Aktif dosyayı bağlamdan çıkar", - "prompt.context.removeFile": "Dosyayı bağlamdan çıkar", - "prompt.action.attachFile": "Dosya ekle", - "prompt.attachment.remove": "Eki kaldır", - "prompt.action.send": "Gönder", - "prompt.action.stop": "Durdur", - - "prompt.toast.pasteUnsupported.title": "Desteklenmeyen ek", - "prompt.toast.pasteUnsupported.description": "Buraya yalnızca resimler, PDF'ler veya metin dosyaları eklenebilir.", - "prompt.toast.modelAgentRequired.title": "Bir ajan ve model seçin", - "prompt.toast.modelAgentRequired.description": "Komut göndermeden önce bir ajan ve model seçin.", - "prompt.toast.worktreeCreateFailed.title": "Çalışma ağacı oluşturulamadı", - "prompt.toast.sessionCreateFailed.title": "Oturum oluşturulamadı", - "prompt.toast.shellSendFailed.title": "Kabuk komutu gönderilemedi", - "prompt.toast.commandSendFailed.title": "Komut gönderilemedi", - "prompt.toast.promptSendFailed.title": "Komut gönderilemedi", - "prompt.toast.promptSendFailed.description": "Oturum alınamadı", - - "dialog.mcp.title": "MCP'ler", - "dialog.mcp.description": "{{total}} içerisinden {{enabled}} etkin", - "dialog.mcp.empty": "Yapılandırılmış MCP yok", - - "dialog.lsp.empty": "LSP'ler dosya türlerinden otomatik algılanır", - "dialog.plugins.empty": "Eklentiler opencode.json içinde yapılandırılır", - - "mcp.status.connected": "bağlı", - "mcp.status.failed": "başarısız", - "mcp.status.needs_auth": "kimlik doğrulama gerekli", - "mcp.status.disabled": "devre dışı", - - "dialog.fork.empty": "Dallandırılacak mesaj yok", - - "dialog.directory.search.placeholder": "Klasör ara", - "dialog.directory.empty": "Klasör bulunamadı", - - "dialog.server.title": "Sunucular", - "dialog.server.description": "Bu uygulamanın hangi Kilo sunucusuna bağlanacağını değiştirin.", - "dialog.server.search.placeholder": "Sunucu ara", - "dialog.server.empty": "Henüz sunucu yok", - "dialog.server.add.title": "Sunucu ekle", - "dialog.server.add.url": "Sunucu URL'si", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "Sunucuya bağlanılamadı", - "dialog.server.add.checking": "Kontrol ediliyor...", - "dialog.server.add.button": "Sunucu ekle", - "dialog.server.add.name": "Sunucu adı (isteğe bağlı)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "Kullanıcı adı (isteğe bağlı)", - "dialog.server.add.password": "Şifre (isteğe bağlı)", - "dialog.server.edit.title": "Sunucuyu düzenle", - "dialog.server.default.title": "Varsayılan sunucu", - "dialog.server.default.description": - "Uygulama başlatıldığında yerel sunucu başlatmak yerine bu sunucuya bağlan. Yeniden başlatma gerektirir.", - "dialog.server.default.none": "Sunucu seçilmedi", - "dialog.server.default.set": "Mevcut sunucuyu varsayılan olarak ayarla", - "dialog.server.default.clear": "Temizle", - "dialog.server.action.remove": "Sunucuyu kaldır", - - "dialog.server.menu.edit": "Düzenle", - "dialog.server.menu.default": "Varsayılan olarak ayarla", - "dialog.server.menu.defaultRemove": "Varsayılanı kaldır", - "dialog.server.menu.delete": "Sil", - "dialog.server.current": "Mevcut Sunucu", - "dialog.server.status.default": "Varsayılan", - - "dialog.project.edit.title": "Projeyi düzenle", - "dialog.project.edit.name": "Ad", - "dialog.project.edit.icon": "Simge", - "dialog.project.edit.icon.alt": "Proje simgesi", - "dialog.project.edit.icon.hint": "Tıkla veya bir görsel sürükle", - "dialog.project.edit.icon.recommended": "Önerilen: 128x128px", - "dialog.project.edit.color": "Renk", - "dialog.project.edit.color.select": "{{color}} rengini seç", - "dialog.project.edit.worktree.startup": "Çalışma alanı başlatma betiği", - "dialog.project.edit.worktree.startup.description": "Yeni bir çalışma alanı (worktree) oluşturduktan sonra çalışır.", - "dialog.project.edit.worktree.startup.placeholder": "örneğin bun install", - - "context.breakdown.title": "Bağlam Dökümü", - "context.breakdown.note": 'Girdi tokenlerinin yaklaşık dökümü. "Diğer" araç tanımları ve ek yükleri içerir.', - "context.breakdown.system": "Sistem", - "context.breakdown.user": "Kullanıcı", - "context.breakdown.assistant": "Asistan", - "context.breakdown.tool": "Araç Çağrıları", - "context.breakdown.other": "Diğer", - - "context.systemPrompt.title": "Sistem Komutu", - "context.rawMessages.title": "Ham mesajlar", - - "context.stats.session": "Oturum", - "context.stats.messages": "Mesajlar", - "context.stats.provider": "Sağlayıcı", - "context.stats.model": "Model", - "context.stats.limit": "Bağlam Limiti", - "context.stats.totalTokens": "Toplam Token", - "context.stats.usage": "Kullanım", - "context.stats.inputTokens": "Girdi Tokenleri", - "context.stats.outputTokens": "Çıktı Tokenleri", - "context.stats.reasoningTokens": "Akıl Yürütme Tokenleri", - "context.stats.cacheTokens": "Önbellek Tokenleri (okuma/yazma)", - "context.stats.userMessages": "Kullanıcı Mesajları", - "context.stats.assistantMessages": "Asistan Mesajları", - "context.stats.totalCost": "Toplam Maliyet", - "context.stats.sessionCreated": "Oturum Oluşturulma", - "context.stats.lastActivity": "Son Etkinlik", - - "context.usage.tokens": "Tokenler", - "context.usage.usage": "Kullanım", - "context.usage.cost": "Maliyet", - "context.usage.clickToView": "Bağlamı görüntüle", - "context.usage.view": "Bağlam kullanımını görüntüle", - - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - - "toast.language.title": "Dil", - "toast.language.description": "{{language}} diline geçildi", - - "toast.theme.title": "Tema değiştirildi", - "toast.scheme.title": "Renk şeması", - - "toast.workspace.enabled.title": "Çalışma alanları etkinleştirildi", - "toast.workspace.enabled.description": "Kenar çubuğunda birden fazla çalışma ağacı gösterilecek", - "toast.workspace.disabled.title": "Çalışma alanları devre dışı bırakıldı", - "toast.workspace.disabled.description": "Kenar çubuğunda yalnızca ana çalışma ağacı gösterilecek", - - "toast.permissions.autoaccept.on.title": "Düzenlemeler otomatik kabul ediliyor", - "toast.permissions.autoaccept.on.description": "Düzenleme ve yazma izinleri otomatik olarak onaylanacak", - "toast.permissions.autoaccept.off.title": "Otomatik kabul durduruldu", - "toast.permissions.autoaccept.off.description": "Düzenleme ve yazma izinleri onay gerektirecek", - - "toast.model.none.title": "Model seçilmedi", - "toast.model.none.description": "Bu oturumu özetlemek için bir sağlayıcı bağlayın", - - "toast.file.loadFailed.title": "Dosya yüklenemedi", - "toast.file.listFailed.title": "Dosyalar listelenemedi", - - "toast.context.noLineSelection.title": "Satır seçimi yok", - "toast.context.noLineSelection.description": "Önce bir dosya sekmesinde satır aralığı seçin.", - - "toast.session.share.copyFailed.title": "URL panoya kopyalanamadı", - "toast.session.share.success.title": "Oturum paylaşıldı", - "toast.session.share.success.description": "Paylaşım URL'si panoya kopyalandı!", - "toast.session.share.failed.title": "Oturum paylaşılamadı", - "toast.session.share.failed.description": "Oturum paylaşılırken bir hata oluştu", - - "toast.session.unshare.success.title": "Oturum paylaşımı kaldırıldı", - "toast.session.unshare.success.description": "Oturum paylaşımı başarıyla kaldırıldı!", - "toast.session.unshare.failed.title": "Oturum paylaşımı kaldırılamadı", - "toast.session.unshare.failed.description": "Oturum paylaşımı kaldırılırken bir hata oluştu", - - "toast.session.listFailed.title": "{{project}} için oturumlar yüklenemedi", - - "toast.update.title": "Güncelleme mevcut", - "toast.update.description": "Kilo'un yeni bir sürümü ({{version}}) yüklemeye hazır.", - "toast.update.action.installRestart": "Yükle ve yeniden başlat", - "toast.update.action.notYet": "Şimdi değil", - - "error.page.title": "Bir şeyler yanlış gitti", - "error.page.description": "Uygulama yüklenirken bir hata oluştu.", - "error.page.details.label": "Hata Detayları", - "error.page.action.restart": "Yeniden Başlat", - "error.page.action.checking": "Kontrol ediliyor...", - "error.page.action.checkUpdates": "Güncellemeleri kontrol et", - "error.page.action.updateTo": "{{version}} sürümüne güncelle", - "error.page.report.prefix": "Lütfen bu hatayı Kilo ekibine bildirin", - "error.page.report.discord": "Discord üzerinden", - "error.page.version": "Sürüm: {{version}}", - - "error.dev.rootNotFound": - "Kök eleman bulunamadı. index.html dosyanıza eklemeyi unuttunuz mu? Ya da id özelliği yanlış mı yazıldı?", - - "error.globalSync.connectFailed": "Sunucuya bağlanılamadı. `{{url}}` adresinde çalışan bir sunucu var mı?", - "directory.error.invalidUrl": "URL'de geçersiz dizin.", - - "error.chain.unknown": "Bilinmeyen hata", - "error.chain.causedBy": "Nedeni:", - "error.chain.apiError": "API hatası", - "error.chain.status": "Durum: {{status}}", - "error.chain.retryable": "Yeniden denenebilir: {{retryable}}", - "error.chain.responseBody": "Yanıt gövdesi:\n{{body}}", - "error.chain.didYouMean": "Bunu mu demek istediniz: {{suggestions}}", - "error.chain.modelNotFound": "Model bulunamadı: {{provider}}/{{model}}", - "error.chain.checkConfig": "Yapılandırma dosyanızı (opencode.json) sağlayıcı/model adlarını kontrol edin", - "error.chain.mcpFailed": - 'MCP sunucusu "{{name}}" başarısız oldu. Not: Kilo henüz MCP kimlik doğrulamasını desteklemiyor.', - "error.chain.providerAuthFailed": "Sağlayıcı kimlik doğrulaması başarısız ({{provider}}): {{message}}", - "error.chain.providerInitFailed": - '"{{provider}}" sağlayıcısı başlatılamadı. Kimlik bilgilerini ve yapılandırmayı kontrol edin.', - "error.chain.configJsonInvalid": "{{path}} adresindeki yapılandırma dosyası geçerli JSON(C) değil", - "error.chain.configJsonInvalidWithMessage": - "{{path}} adresindeki yapılandırma dosyası geçerli JSON(C) değil: {{message}}", - "error.chain.configDirectoryTypo": - '"{{dir}}" dizini {{path}} içinde geçerli değil. Dizini "{{suggestion}}" olarak yeniden adlandırın veya kaldırın. Bu yaygın bir yazım hatasıdır.', - "error.chain.configFrontmatterError": "{{path}} içindeki ön bilgi ayrıştırılamadı:\n{{message}}", - "error.chain.configInvalid": "{{path}} adresindeki yapılandırma dosyası geçersiz", - "error.chain.configInvalidWithMessage": "{{path}} adresindeki yapılandırma dosyası geçersiz: {{message}}", - - "notification.permission.title": "İzin gerekli", - "notification.permission.description": "{{projectName}} içindeki {{sessionTitle}} izin gerektiriyor", - "notification.question.title": "Soru", - "notification.question.description": "{{projectName}} içindeki {{sessionTitle}} bir soru soruyor", - "notification.action.goToSession": "Oturuma git", - - "notification.session.responseReady.title": "Yanıt hazır", - "notification.session.error.title": "Oturum hatası", - "notification.session.error.fallbackDescription": "Bir hata oluştu", - - "home.recentProjects": "Son projeler", - "home.empty.title": "Son proje yok", - "home.empty.description": "Yerel bir proje açarak başlayın", - - "session.tab.session": "Oturum", - "session.tab.review": "İnceleme", - "session.tab.context": "Bağlam", - "session.panel.reviewAndFiles": "İnceleme ve dosyalar", - "session.review.filesChanged": "{{count}} Dosya Değişti", - "session.review.change.one": "Değişiklik", - "session.review.change.other": "Değişiklik", - "session.review.loadingChanges": "Değişiklikler yükleniyor...", - "session.review.empty": "Bu oturumda henüz değişiklik yok", - "session.review.noVcs": "Git VCS algılanamadı, oturum değişiklikleri tespit edilemeyecek", - "session.review.noSnapshot": - "Yapılandırmada anlık görüntü takibi devre dışı bırakıldı, bu nedenle oturum değişiklikleri kullanılamıyor", - "session.review.noChanges": "Değişiklik yok", - - "session.files.selectToOpen": "Açmak için bir dosya seçin", - "session.files.all": "Tüm dosyalar", - "session.files.empty": "Dosya yok", - "session.files.binaryContent": "İkili dosya (içerik görüntülenemiyor)", - - "session.messages.renderEarlier": "Önceki mesajları göster", - "session.messages.loadingEarlier": "Önceki mesajlar yükleniyor...", - "session.messages.loadEarlier": "Önceki mesajları yükle", - "session.messages.loading": "Mesajlar yükleniyor...", - "session.messages.jumpToLatest": "En sona atla", - - "session.context.addToContext": "{{selection}} bağlama ekle", - "session.todo.title": "Görevler", - "session.todo.collapse": "Daralt", - "session.todo.expand": "Genişlet", - "session.followupDock.summary.one": "{{count}} sıradaki mesaj", - "session.followupDock.summary.other": "{{count}} sıradaki mesaj", - "session.followupDock.sendNow": "Şimdi gönder", - "session.followupDock.edit": "Düzenle", - "session.followupDock.collapse": "Sıradaki mesajları daralt", - "session.followupDock.expand": "Sıradaki mesajları genişlet", - "session.revertDock.summary.one": "{{count}} geri alınan mesaj", - "session.revertDock.summary.other": "{{count}} geri alınan mesaj", - "session.revertDock.collapse": "Geri alınan mesajları daralt", - "session.revertDock.expand": "Geri alınan mesajları genişlet", - "session.revertDock.restore": "Mesajı geri yükle", - - "session.new.title": "İstediğini yap", - "session.new.worktree.main": "Ana dal", - "session.new.worktree.mainWithBranch": "Ana dal ({{branch}})", - "session.new.worktree.create": "Yeni çalışma ağacı oluştur", - "session.new.lastModified": "Son değişiklik", - - "session.header.search.placeholder": "{{project}} ara", - "session.header.searchFiles": "Dosya ara", - "session.header.openIn": "Aç", - "session.header.open.action": "{{app}} ile aç", - "session.header.open.ariaLabel": "{{app}} ile aç", - "session.header.open.menu": "Açma seçenekleri", - "session.header.open.copyPath": "Yolu kopyala", - - "status.popover.trigger": "Durum", - "status.popover.ariaLabel": "Sunucu yapılandırmaları", - "status.popover.tab.servers": "Sunucular", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "Eklentiler", - "status.popover.action.manageServers": "Sunucuları yönet", - - "session.share.popover.title": "Web'de yayınla", - "session.share.popover.description.shared": "Bu oturum web'de herkese açıktır. Bağlantıya sahip herkes erişebilir.", - "session.share.popover.description.unshared": - "Oturumu web'de herkese açık olarak paylaşın. Bağlantıya sahip herkes erişebilecek.", - "session.share.action.share": "Paylaş", - "session.share.action.publish": "Yayınla", - "session.share.action.publishing": "Yayınlanıyor...", - "session.share.action.unpublish": "Yayından kaldır", - "session.share.action.unpublishing": "Yayından kaldırılıyor...", - "session.share.action.view": "Görüntüle", - "session.share.copy.copied": "Kopyalandı", - "session.share.copy.copyLink": "Bağlantı kopyala", - - "lsp.tooltip.none": "LSP sunucusu yok", - "lsp.label.connected": "{{count}} LSP", - - "prompt.loading": "Komut yükleniyor...", - "terminal.loading": "Terminal yükleniyor...", - "terminal.title": "Terminal", - "terminal.title.numbered": "Terminal {{number}}", - "terminal.close": "Terminali kapat", - "terminal.connectionLost.title": "Bağlantı Kesildi", - "terminal.connectionLost.description": - "Terminal bağlantısı kesildi. Bu durum sunucu yeniden başladığında oluşabilir.", - - "common.closeTab": "Sekmeyi kapat", - "common.dismiss": "Kapat", - "common.requestFailed": "İstek başarısız", - "common.moreOptions": "Daha fazla seçenek", - "common.learnMore": "Daha fazla bilgi", - "common.rename": "Yeniden adlandır", - "common.reset": "Sıfırla", - "common.archive": "Arşivle", - "common.delete": "Sil", - "common.close": "Kapat", - "common.edit": "Düzenle", - "common.loadMore": "Daha fazla yükle", - "common.key.esc": "ESC", - - "sidebar.menu.toggle": "Menüyü aç/kapat", - "sidebar.nav.projectsAndSessions": "Projeler ve oturumlar", - "sidebar.settings": "Ayarlar", - "sidebar.help": "Yardım", - "sidebar.workspaces.enable": "Çalışma alanlarını etkinleştir", - "sidebar.workspaces.disable": "Çalışma alanlarını devre dışı bırak", - "sidebar.gettingStarted.title": "Başlarken", - "sidebar.gettingStarted.line1": "Kilo ücretsiz modeller içerir, böylece hemen başlayabilirsiniz.", - "sidebar.gettingStarted.line2": "Claude, GPT, Gemini vb. modelleri kullanmak için herhangi bir sağlayıcı bağlayın.", - "sidebar.project.recentSessions": "Son oturumlar", - "sidebar.project.viewAllSessions": "Tüm oturumları görüntüle", - "sidebar.project.clearNotifications": "Bildirimleri temizle", - - "app.name.desktop": "Kilo Masaüstü", - - "settings.section.desktop": "Masaüstü", - "settings.section.server": "Sunucu", - "settings.tab.general": "Genel", - "settings.tab.shortcuts": "Kısayollar", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL entegrasyonu", - "settings.desktop.wsl.description": "Kilo sunucusunu Windows'ta WSL içinde çalıştırın.", - - "settings.general.section.appearance": "Görünüm", - "settings.general.section.notifications": "Sistem bildirimleri", - "settings.general.section.updates": "Güncellemeler", - "settings.general.section.sounds": "Ses efektleri", - "settings.general.section.feed": "Akış", - "settings.general.section.display": "Ekran", - - "settings.general.row.language.title": "Dil", - "settings.general.row.language.description": "Kilo'un görünüm dilini değiştirin", - "settings.general.row.appearance.title": "Görünüm", - "settings.general.row.appearance.description": "Kilo'un cihazınızdaki görünümünü özelleştirin", - "settings.general.row.colorScheme.title": "Renk şeması", - "settings.general.row.colorScheme.description": "Kilo'un sistem, açık veya koyu temayı takip etip etmeyeceğini seçin", - "settings.general.row.theme.title": "Tema", - "settings.general.row.theme.description": "Kilo'un temasını özelleştirin.", - "settings.general.row.font.title": "Kod Yazı Tipi", - "settings.general.row.font.description": "Kod bloklarında kullanılan yazı tipini özelleştirin", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "Arayüz Yazı Tipi", - "settings.general.row.uiFont.description": "Arayüz genelinde kullanılan yazı tipini özelleştirin", - "settings.general.row.followup.title": "Takip davranışı", - "settings.general.row.followup.description": - "Takip komutlarının hemen yönlendirilmesini mi yoksa sırada beklemesini mi istediğinizi seçin", - "settings.general.row.followup.option.queue": "Sıra", - "settings.general.row.followup.option.steer": "Yönlendir", - "settings.general.row.reasoningSummaries.title": "Akıl yürütme özetlerini göster", - "settings.general.row.reasoningSummaries.description": "Zaman çizelgesinde model akıl yürütme özetlerini görüntüle", - "settings.general.row.shellToolPartsExpanded.title": "Kabuk araç bileşenlerini genişlet", - "settings.general.row.shellToolPartsExpanded.description": - "Zaman çizelgesinde kabuk araç bileşenlerini varsayılan olarak genişletilmiş göster", - "settings.general.row.editToolPartsExpanded.title": "Düzenleme araç bileşenlerini genişlet", - "settings.general.row.editToolPartsExpanded.description": - "Zaman çizelgesinde düzenleme, yazma ve yama araç bileşenlerini varsayılan olarak genişletilmiş göster", - - "settings.general.row.showSessionProgressBar.title": "Oturum ilerleme çubuğunu göster", - "settings.general.row.showSessionProgressBar.description": - "Ajan çalışırken oturumun üst kısmında animasyonlu ilerleme çubuğunu göster", - - "settings.general.row.wayland.title": "Yerel Wayland kullan", - "settings.general.row.wayland.description": - "Wayland'da X11 geri dönüşünü devre dışı bırak. Yeniden başlatma gerektirir.", - "settings.general.row.wayland.tooltip": - "Karışık yenileme hızlı monitörlere sahip Linux'ta yerel Wayland daha kararlı olabilir.", - - "settings.general.row.releaseNotes.title": "Sürüm notları", - "settings.general.row.releaseNotes.description": "Güncellemelerden sonra Yenilikler bildirimlerini göster", - - "settings.updates.row.startup.title": "Başlangıçta güncellemeleri kontrol et", - "settings.updates.row.startup.description": "Kilo başladığında otomatik güncelleme kontrolü yap", - "settings.updates.row.check.title": "Güncellemeleri kontrol et", - "settings.updates.row.check.description": "Elle güncelleme kontrolü yap ve varsa yükle", - "settings.updates.action.checkNow": "Şimdi kontrol et", - "settings.updates.action.checking": "Kontrol ediliyor...", - "settings.updates.toast.latest.title": "Güncelsiniz", - "settings.updates.toast.latest.description": "Kilo'un en son sürümünü kullanıyorsunuz.", - - "sound.option.none": "Yok", - "sound.option.alert01": "Uyarı 01", - "sound.option.alert02": "Uyarı 02", - "sound.option.alert03": "Uyarı 03", - "sound.option.alert04": "Uyarı 04", - "sound.option.alert05": "Uyarı 05", - "sound.option.alert06": "Uyarı 06", - "sound.option.alert07": "Uyarı 07", - "sound.option.alert08": "Uyarı 08", - "sound.option.alert09": "Uyarı 09", - "sound.option.alert10": "Uyarı 10", - "sound.option.bipbop01": "Bip-bop 01", - "sound.option.bipbop02": "Bip-bop 02", - "sound.option.bipbop03": "Bip-bop 03", - "sound.option.bipbop04": "Bip-bop 04", - "sound.option.bipbop05": "Bip-bop 05", - "sound.option.bipbop06": "Bip-bop 06", - "sound.option.bipbop07": "Bip-bop 07", - "sound.option.bipbop08": "Bip-bop 08", - "sound.option.bipbop09": "Bip-bop 09", - "sound.option.bipbop10": "Bip-bop 10", - "sound.option.staplebops01": "Staplebops 01", - "sound.option.staplebops02": "Staplebops 02", - "sound.option.staplebops03": "Staplebops 03", - "sound.option.staplebops04": "Staplebops 04", - "sound.option.staplebops05": "Staplebops 05", - "sound.option.staplebops06": "Staplebops 06", - "sound.option.staplebops07": "Staplebops 07", - "sound.option.nope01": "Hayır 01", - "sound.option.nope02": "Hayır 02", - "sound.option.nope03": "Hayır 03", - "sound.option.nope04": "Hayır 04", - "sound.option.nope05": "Hayır 05", - "sound.option.nope06": "Hayır 06", - "sound.option.nope07": "Hayır 07", - "sound.option.nope08": "Hayır 08", - "sound.option.nope09": "Hayır 09", - "sound.option.nope10": "Hayır 10", - "sound.option.nope11": "Hayır 11", - "sound.option.nope12": "Hayır 12", - "sound.option.yup01": "Evet 01", - "sound.option.yup02": "Evet 02", - "sound.option.yup03": "Evet 03", - "sound.option.yup04": "Evet 04", - "sound.option.yup05": "Evet 05", - "sound.option.yup06": "Evet 06", - - "settings.general.notifications.agent.title": "Ajan", - "settings.general.notifications.agent.description": - "Ajan tamamlandığında veya dikkat gerektirdiğinde sistem bildirimi göster", - "settings.general.notifications.permissions.title": "İzinler", - "settings.general.notifications.permissions.description": "İzin gerektiğinde sistem bildirimi göster", - "settings.general.notifications.errors.title": "Hatalar", - "settings.general.notifications.errors.description": "Hata oluştuğunda sistem bildirimi göster", - - "settings.general.sounds.agent.title": "Ajan", - "settings.general.sounds.agent.description": "Ajan tamamlandığında veya dikkat gerektirdiğinde ses çal", - "settings.general.sounds.permissions.title": "İzinler", - "settings.general.sounds.permissions.description": "İzin gerektiğinde ses çal", - "settings.general.sounds.errors.title": "Hatalar", - "settings.general.sounds.errors.description": "Hata oluştuğunda ses çal", - - "settings.shortcuts.title": "Klavye kısayolları", - "settings.shortcuts.reset.button": "Varsayılanlara sıfırla", - "settings.shortcuts.reset.toast.title": "Kısayollar sıfırlandı", - "settings.shortcuts.reset.toast.description": "Klavye kısayolları varsayılanlara sıfırlandı.", - "settings.shortcuts.conflict.title": "Kısayol zaten kullanılıyor", - "settings.shortcuts.conflict.description": "{{keybind}} zaten {{titles}} için atanmış.", - "settings.shortcuts.unassigned": "Atanmamış", - "settings.shortcuts.pressKeys": "Tuşlara basın", - "settings.shortcuts.search.placeholder": "Kısayol ara", - "settings.shortcuts.search.empty": "Kısayol bulunamadı", - - "settings.shortcuts.group.general": "Genel", - "settings.shortcuts.group.session": "Oturum", - "settings.shortcuts.group.navigation": "Gezinme", - "settings.shortcuts.group.modelAndAgent": "Model ve ajan", - "settings.shortcuts.group.terminal": "Terminal", - "settings.shortcuts.group.prompt": "Komut", - - "settings.providers.title": "Sağlayıcılar", - "settings.providers.description": "Sağlayıcı ayarları burada yapılandırılabilecek.", - "settings.providers.section.connected": "Bağlı sağlayıcılar", - "settings.providers.connected.empty": "Bağlı sağlayıcı yok", - "settings.providers.section.popular": "Popüler sağlayıcılar", - "settings.providers.tag.environment": "Ortam", - "settings.providers.tag.config": "Yapılandırma", - "settings.providers.tag.custom": "Özel", - "settings.providers.tag.other": "Diğer", - "settings.models.title": "Modeller", - "settings.models.description": "Model ayarları burada yapılandırılabilecek.", - "settings.agents.title": "Ajanlar", - "settings.agents.description": "Ajan ayarları burada yapılandırılabilecek.", - "settings.commands.title": "Komutlar", - "settings.commands.description": "Komut ayarları burada yapılandırılabilecek.", - "settings.mcp.title": "MCP", - "settings.mcp.description": "MCP ayarları burada yapılandırılabilecek.", - - "settings.permissions.title": "İzinler", - "settings.permissions.description": "Sunucunun varsayılan olarak hangi araçları kullanabileceğini kontrol edin.", - "settings.permissions.section.tools": "Araçlar", - "settings.permissions.toast.updateFailed.title": "İzinler güncellenemedi", - - "settings.permissions.action.allow": "İzin Ver", - "settings.permissions.action.ask": "Sor", - "settings.permissions.action.deny": "Reddet", - - "settings.permissions.tool.read.title": "Oku", - "settings.permissions.tool.read.description": "Bir dosyayı okuma (dosya yoluyla eşleşir)", - "settings.permissions.tool.edit.title": "Düzenle", - "settings.permissions.tool.edit.description": "Düzenleme, yazma, yama ve çoklu düzenleme dahil dosyaları değiştir", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "Glob kalıpları kullanarak dosyaları eşle", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "Düzenli ifadeler kullanarak dosya içerikleri ara", - "settings.permissions.tool.list.title": "Listele", - "settings.permissions.tool.list.description": "Bir dizindeki dosyaları listele", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "Kabuk komutları çalıştır", - "settings.permissions.tool.task.title": "Görev", - "settings.permissions.tool.task.description": "Alt ajanlar başlat", - "settings.permissions.tool.skill.title": "Beceri", - "settings.permissions.tool.skill.description": "Ada göre bir beceri yükle", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "Dil sunucusu sorguları çalıştır", - "settings.permissions.tool.todowrite.title": "Görev Yaz", - "settings.permissions.tool.todowrite.description": "Görev listesini güncelle", - "settings.permissions.tool.webfetch.title": "Web Getir", - "settings.permissions.tool.webfetch.description": "Bir URL'den içerik getir", - "settings.permissions.tool.websearch.title": "Web Ara", - "settings.permissions.tool.websearch.description": "Web'de ara", - "settings.permissions.tool.codesearch.title": "Kod Ara", - "settings.permissions.tool.codesearch.description": "Web'de kod ara", - "settings.permissions.tool.external_directory.title": "Harici Dizin", - "settings.permissions.tool.external_directory.description": "Proje dizini dışındaki dosyalara eriş", - "settings.permissions.tool.doom_loop.title": "Sonsuz Döngü", - "settings.permissions.tool.doom_loop.description": "Aynı girdiyle tekrarlanan araç çağrılarını algıla", - - "session.delete.failed.title": "Oturum silinemedi", - "session.delete.title": "Oturumu sil", - "session.delete.confirm": '"{{name}}" oturumu silinsin mi?', - "session.delete.button": "Oturumu sil", - - "workspace.new": "Yeni çalışma alanı", - "workspace.type.local": "yerel", - "workspace.type.sandbox": "sandbox", - "workspace.create.failed.title": "Çalışma alanı oluşturulamadı", - "workspace.delete.failed.title": "Çalışma alanı silinemedi", - "workspace.resetting.title": "Çalışma alanı sıfırlanıyor", - "workspace.resetting.description": "Bu bir dakika sürebilir.", - "workspace.reset.failed.title": "Çalışma alanı sıfırlanamadı", - "workspace.reset.success.title": "Çalışma alanı sıfırlandı", - "workspace.reset.success.description": "Çalışma alanı artık varsayılan dalla eşleşiyor.", - "workspace.error.stillPreparing": "Çalışma alanı hâlâ hazırlanıyor", - "workspace.status.checking": "Birleşmemiş değişiklikler kontrol ediliyor...", - "workspace.status.error": "Git durumu doğrulanamadı.", - "workspace.status.clean": "Birleşmemiş değişiklik algılanmadı.", - "workspace.status.dirty": "Bu çalışma alanında birleşmemiş değişiklikler algılandı.", - "workspace.delete.title": "Çalışma alanını sil", - "workspace.delete.confirm": '"{{name}}" çalışma alanı silinsin mi?', - "workspace.delete.button": "Çalışma alanını sil", - "workspace.reset.title": "Çalışma alanını sıfırla", - "workspace.reset.confirm": '"{{name}}" çalışma alanı sıfırlansın mı?', - "workspace.reset.button": "Çalışma alanını sıfırla", - "workspace.reset.archived.none": "Arşivlenecek aktif oturum yok.", - "workspace.reset.archived.one": "1 oturum arşivlenecek.", - "workspace.reset.archived.many": "{{count}} oturum arşivlenecek.", - "workspace.reset.note": "Bu işlem çalışma alanını varsayılan dalla eşleşecek şekilde sıfırlayacak.", - "common.open": "Aç", - "dialog.releaseNotes.action.getStarted": "Başla", - "dialog.releaseNotes.action.next": "İleri", - "dialog.releaseNotes.action.hideFuture": "Bunu gelecekte bir daha gösterme", - "dialog.releaseNotes.media.alt": "Sürüm önizlemesi", - "toast.project.reloadFailed.title": "{{project}} yeniden yüklenemedi", - "error.server.invalidConfiguration": "Geçersiz yapılandırma", - "common.moreCountSuffix": " (+{{count}} daha)", - "common.time.justNow": "Şimdi", - "common.time.minutesAgo.short": "{{count}}dk önce", - "common.time.hoursAgo.short": "{{count}}sa önce", - "common.time.daysAgo.short": "{{count}}g önce", - "settings.providers.connected.environmentDescription": "Ortam değişkenlerinizden bağlandı", - "settings.providers.custom.description": "Temel URL üzerinden OpenAI uyumlu bir sağlayıcı ekleyin.", - - "app.server.unreachable": "{{server}} sunucusuna ulaşılamadı", - "app.server.retrying": "Otomatik olarak tekrar deneniyor...", - "app.server.otherServers": "Diğer sunucular", - "dialog.server.add.usernamePlaceholder": "kullanıcı adı", - "dialog.server.add.passwordPlaceholder": "parola", - "server.row.noUsername": "kullanıcı adı yok", - "session.review.noVcs.createGit.title": "Git deposu oluştur", - "session.review.noVcs.createGit.description": "Bu projedeki değişiklikleri takip et, incele ve geri al", - "session.review.noVcs.createGit.actionLoading": "Git deposu oluşturuluyor...", - "session.review.noVcs.createGit.action": "Git deposu oluştur", - "session.todo.progress": "{{total}} görevin {{done}} tanesi tamamlandı", - "session.question.progress": "{{total}} sorunun {{current}} tanesi", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "Dosya Gezgini", - "session.header.open.fileManager": "Dosya Yöneticisi", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "Terminal", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "Geliştirme performansı teşhisi", - "debugBar.na": "yok", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": - "Yönlendirici başlangıcından yerleşme sonrası ilk boyamaya kadar ölçülen, bir oturum sayfasına dokunan son tamamlanmış rota geçişi.", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "Son 5 saniyedeki kayan saniye başına kare sayısı.", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "Son 5 saniyedeki en kötü kare süresi.", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "Son 5 saniyede 32ms üzerindeki kareler.", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "Son 5 saniyedeki engellenen süre ve uzun görev sayısı. Maksimum görev: {{max}}.", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "Son 5 saniyede gözlemlenen en kötü giriş gecikmesi.", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": "Son 5 saniyedeki yaklaşık etkileşim süresi. Bu INP benzeridir, resmi Web Vitals INP değildir.", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "Mevcut uygulama ömrü için kümülatif düzen kayması.", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "Kullanılan JS yığını vs yığın sınırı. Yalnızca Chromium.", - "debugBar.mem.tip": "Kullanılan JS yığını vs yığın sınırı. {{limit}} içinde {{used}}.", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "Boşluk", - "common.key.backspace": "Geri", - "common.key.enter": "Enter", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "bilinmiyor", - "error.page.circular": "[Döngüsel]", - "error.globalSDK.noServerAvailable": "Sunucu yok", - "error.globalSDK.serverNotAvailable": "Sunucu mevcut değil", - "error.childStore.persistedCacheCreateFailed": "Kalıcı önbellek oluşturulamadı", - "error.childStore.persistedProjectMetadataCreateFailed": "Kalıcı proje meta verileri oluşturulamadı", - "error.childStore.persistedProjectIconCreateFailed": "Kalıcı proje simgesi oluşturulamadı", - "error.childStore.storeCreateFailed": "Depo oluşturulamadı", - "terminal.connectionLost.abnormalClose": "WebSocket anormal şekilde kapandı: {{code}}", -} satisfies Partial> diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts deleted file mode 100644 index 52c20f9b5e7..00000000000 --- a/packages/app/src/i18n/zh.ts +++ /dev/null @@ -1,920 +0,0 @@ -import { dict as en } from "./en" - -type Keys = keyof typeof en - -export const dict = { - "command.category.suggested": "建议", - "command.category.view": "视图", - "command.category.project": "项目", - "command.category.provider": "提供商", - "command.category.server": "服务器", - "command.category.session": "会话", - "command.category.theme": "主题", - "command.category.language": "语言", - "command.category.file": "文件", - "command.category.context": "上下文", - "command.category.terminal": "终端", - "command.category.model": "模型", - "command.category.mcp": "MCP", - "command.category.agent": "智能体", - "command.category.permissions": "权限", - "command.category.workspace": "工作区", - "command.category.settings": "设置", - - "theme.scheme.system": "系统", - "theme.scheme.light": "浅色", - "theme.scheme.dark": "深色", - - "command.sidebar.toggle": "切换侧边栏", - - "command.project.open": "打开项目", - - "command.provider.connect": "连接提供商", - - "command.server.switch": "切换服务器", - - "command.settings.open": "打开设置", - - "command.session.previous": "上一个会话", - "command.session.next": "下一个会话", - "command.session.previous.unseen": "上一个未读会话", - "command.session.next.unseen": "下一个未读会话", - "command.session.archive": "归档会话", - - "command.palette": "命令面板", - - "command.theme.cycle": "切换主题", - "command.theme.set": "使用主题:{{theme}}", - "command.theme.scheme.cycle": "切换配色方案", - "command.theme.scheme.set": "使用配色方案:{{scheme}}", - - "command.language.cycle": "切换语言", - "command.language.set": "使用语言:{{language}}", - - "command.session.new": "新建会话", - - "command.file.open": "打开文件", - - "command.tab.close": "关闭标签页", - - "command.context.addSelection": "将所选内容添加到上下文", - "command.context.addSelection.description": "添加当前文件中选中的行", - - "command.input.focus": "聚焦输入框", - - "command.terminal.toggle": "切换终端", - - "command.fileTree.toggle": "切换文件树", - - "command.review.toggle": "切换审查", - - "command.terminal.new": "新建终端", - "command.terminal.new.description": "创建新的终端标签页", - - "command.steps.toggle": "切换步骤", - "command.steps.toggle.description": "显示或隐藏当前消息的步骤", - - "command.message.previous": "上一条消息", - "command.message.previous.description": "跳转到上一条用户消息", - "command.message.next": "下一条消息", - "command.message.next.description": "跳转到下一条用户消息", - - "command.model.choose": "选择模型", - "command.model.choose.description": "选择不同的模型", - - "command.mcp.toggle": "切换 MCPs", - "command.mcp.toggle.description": "切换 MCPs", - - "command.agent.cycle": "切换智能体", - "command.agent.cycle.description": "切换到下一个智能体", - "command.agent.cycle.reverse": "反向切换智能体", - "command.agent.cycle.reverse.description": "切换到上一个智能体", - - "command.model.variant.cycle": "切换思考强度", - "command.model.variant.cycle.description": "切换到下一个强度等级", - - "command.prompt.mode.shell": "Shell", - "command.prompt.mode.normal": "Prompt", - - "command.permissions.autoaccept.enable": "自动接受权限", - "command.permissions.autoaccept.disable": "停止自动接受权限", - - "command.workspace.toggle": "切换工作区", - "command.workspace.toggle.description": "在侧边栏启用或禁用多个工作区", - - "command.session.undo": "撤销", - "command.session.undo.description": "撤销上一条消息", - "command.session.redo": "重做", - "command.session.redo.description": "重做上一条撤销的消息", - "command.session.compact": "精简会话", - "command.session.compact.description": "总结会话以减少上下文大小", - "command.session.fork": "从消息分叉", - "command.session.fork.description": "从之前的消息创建新会话", - "command.session.share": "分享会话", - "command.session.share.description": "分享此会话并将链接复制到剪贴板", - "command.session.unshare": "取消分享会话", - "command.session.unshare.description": "停止分享此会话", - - "palette.search.placeholder": "搜索文件、命令和会话", - "palette.empty": "未找到结果", - "palette.group.commands": "命令", - "palette.group.files": "文件", - - "dialog.provider.search.placeholder": "搜索提供商", - "dialog.provider.empty": "未找到提供商", - "dialog.provider.group.popular": "热门", - "dialog.provider.group.other": "其他", - "dialog.provider.tag.recommended": "推荐", - "dialog.provider.opencode.note": "使用 OpenCode Zen 或 API 密钥连接", - "dialog.provider.opencode.tagline": "可靠的优化模型", - "dialog.provider.opencodeGo.tagline": "适合所有人的低成本订阅", - "dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接", - "dialog.provider.copilot.note": "使用 Copilot 或 API 密钥连接", - "dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接", - "dialog.provider.google.note": "使用 Google 账号或 API 密钥连接", - "dialog.provider.openrouter.note": "使用 OpenRouter 账号或 API 密钥连接", - "dialog.provider.vercel.note": "使用 Vercel 账号或 API 密钥连接", - - "dialog.model.select.title": "选择模型", - "dialog.model.search.placeholder": "搜索模型", - "dialog.model.empty": "未找到模型", - "dialog.model.manage": "管理模型", - "dialog.model.manage.description": "自定义模型选择器中显示的模型。", - "dialog.model.manage.provider.toggle": "切换所有 {{provider}} 模型", - "dialog.model.unpaid.freeModels.title": "Kilo 提供的免费模型", - "dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型", - - "dialog.provider.viewAll": "查看更多提供商", - - "provider.connect.title": "连接 {{provider}}", - "provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登录", - "provider.connect.selectMethod": "选择 {{provider}} 的登录方式。", - "provider.connect.method.apiKey": "API 密钥", - "provider.connect.status.inProgress": "正在授权...", - "provider.connect.status.waiting": "等待授权...", - "provider.connect.status.failed": "授权失败:{{error}}", - "provider.connect.apiKey.description": - "输入你的 {{provider}} API 密钥以连接帐户,并在 Kilo 中使用 {{provider}} 模型。", - "provider.connect.apiKey.label": "{{provider}} API 密钥", - "provider.connect.apiKey.placeholder": "API 密钥", - "provider.connect.apiKey.required": "API 密钥为必填项", - "provider.connect.opencodeZen.line1": "OpenCode Zen 为你提供一组精选的可靠优化模型,用于代码智能体。", - "provider.connect.opencodeZen.line2": "只需一个 API 密钥,你就能使用 Claude、GPT、Gemini、GLM 等模型。", - "provider.connect.opencodeZen.visit.prefix": "访问 ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " 获取你的 API 密钥。", - "provider.connect.oauth.code.visit.prefix": "访问 ", - "provider.connect.oauth.code.visit.link": "此链接", - "provider.connect.oauth.code.visit.suffix": " 获取授权码,以连接你的帐户并在 Kilo 中使用 {{provider}} 模型。", - "provider.connect.oauth.code.label": "{{method}} 授权码", - "provider.connect.oauth.code.placeholder": "授权码", - "provider.connect.oauth.code.required": "授权码为必填项", - "provider.connect.oauth.code.invalid": "授权码无效", - "provider.connect.oauth.auto.visit.prefix": "访问 ", - "provider.connect.oauth.auto.visit.link": "此链接", - "provider.connect.oauth.auto.visit.suffix": " 并输入以下代码,以连接你的帐户并在 Kilo 中使用 {{provider}} 模型。", - "provider.connect.oauth.auto.confirmationCode": "确认码", - "provider.connect.toast.connected.title": "{{provider}} 已连接", - "provider.connect.toast.connected.description": "现在可以使用 {{provider}} 模型了。", - - "provider.custom.title": "自定义提供商", - "provider.custom.description.prefix": "配置与 OpenAI 兼容的提供商。请查看", - "provider.custom.description.link": "提供商配置文档", - "provider.custom.description.suffix": "。", - "provider.custom.field.providerID.label": "提供商 ID", - "provider.custom.field.providerID.placeholder": "myprovider", - "provider.custom.field.providerID.description": "使用小写字母、数字、连字符或下划线", - "provider.custom.field.name.label": "显示名称", - "provider.custom.field.name.placeholder": "我的 AI 提供商", - "provider.custom.field.baseURL.label": "基础 URL", - "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", - "provider.custom.field.apiKey.label": "API 密钥", - "provider.custom.field.apiKey.placeholder": "API 密钥", - "provider.custom.field.apiKey.description": "可选。如果你通过请求头管理认证,可留空。", - "provider.custom.models.label": "模型", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "model-id", - "provider.custom.models.name.label": "名称", - "provider.custom.models.name.placeholder": "显示名称", - "provider.custom.models.remove": "移除模型", - "provider.custom.models.add": "添加模型", - "provider.custom.headers.label": "请求头(可选)", - "provider.custom.headers.key.label": "请求头", - "provider.custom.headers.key.placeholder": "Header-Name", - "provider.custom.headers.value.label": "值", - "provider.custom.headers.value.placeholder": "value", - "provider.custom.headers.remove": "移除请求头", - "provider.custom.headers.add": "添加请求头", - "provider.custom.error.providerID.required": "提供商 ID 为必填项", - "provider.custom.error.providerID.format": "请使用小写字母、数字、连字符或下划线", - "provider.custom.error.providerID.exists": "该提供商 ID 已存在", - "provider.custom.error.name.required": "显示名称为必填项", - "provider.custom.error.baseURL.required": "基础 URL 为必填项", - "provider.custom.error.baseURL.format": "必须以 http:// 或 https:// 开头", - "provider.custom.error.required": "必填", - "provider.custom.error.duplicate": "重复", - - "provider.disconnect.toast.disconnected.title": "{{provider}} 已断开连接", - "provider.disconnect.toast.disconnected.description": "{{provider}} 模型已不再可用。", - - "model.tag.free": "免费", - "model.tag.latest": "最新", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "文本", - "model.input.image": "图像", - "model.input.audio": "音频", - "model.input.video": "视频", - "model.input.pdf": "pdf", - "model.tooltip.allows": "支持:{{inputs}}", - "model.tooltip.reasoning.allowed": "支持推理", - "model.tooltip.reasoning.none": "不支持推理", - "model.tooltip.context": "上下文上限 {{limit}}", - - "common.search.placeholder": "搜索", - "common.goBack": "返回", - "common.goForward": "前进", - "common.loading": "加载中", - "common.loading.ellipsis": "...", - "common.cancel": "取消", - "common.connect": "连接", - "common.disconnect": "断开连接", - "common.continue": "提交", - "common.submit": "提交", - "common.save": "保存", - "common.saving": "保存中...", - "common.default": "默认", - "common.attachment": "附件", - - "prompt.placeholder.shell": "输入 shell 命令... {{example}}", - "prompt.placeholder.normal": '随便问点什么... "{{example}}"', - "prompt.placeholder.simple": "随便问点什么...", - "prompt.placeholder.summarizeComments": "总结评论…", - "prompt.placeholder.summarizeComment": "总结该评论…", - "prompt.mode.shell": "Shell", - "prompt.mode.normal": "Prompt", - "prompt.mode.shell.exit": "按 esc 退出", - "prompt.example.1": "修复代码库中的一个 TODO", - "prompt.example.2": "这个项目的技术栈是什么?", - "prompt.example.3": "修复失败的测试", - "prompt.example.4": "解释认证是如何工作的", - "prompt.example.5": "查找并修复安全漏洞", - "prompt.example.6": "为用户服务添加单元测试", - "prompt.example.7": "重构这个函数,让它更易读", - "prompt.example.8": "这个错误是什么意思?", - "prompt.example.9": "帮我调试这个问题", - "prompt.example.10": "生成 API 文档", - "prompt.example.11": "优化数据库查询", - "prompt.example.12": "添加输入校验", - "prompt.example.13": "创建一个新的组件用于...", - "prompt.example.14": "我该如何部署这个项目?", - "prompt.example.15": "审查我的代码并给出最佳实践建议", - "prompt.example.16": "为这个函数添加错误处理", - "prompt.example.17": "解释这个正则表达式", - "prompt.example.18": "把它转换成 TypeScript", - "prompt.example.19": "在整个代码库中添加日志", - "prompt.example.20": "哪些依赖已经过期?", - "prompt.example.21": "帮我写一个迁移脚本", - "prompt.example.22": "为这个接口实现缓存", - "prompt.example.23": "给这个列表添加分页", - "prompt.example.24": "创建一个 CLI 命令用于...", - "prompt.example.25": "这里的环境变量是怎么工作的?", - "prompt.popover.emptyResults": "没有匹配的结果", - "prompt.popover.emptyCommands": "没有匹配的命令", - "prompt.dropzone.label": "将图片、PDF 或文本文件拖放到此处", - "prompt.dropzone.file.label": "拖放以 @提及文件", - "prompt.slash.badge.custom": "自定义", - "prompt.slash.badge.skill": "技能", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "当前", - "prompt.context.includeActiveFile": "包含当前文件", - "prompt.context.removeActiveFile": "从上下文移除活动文件", - "prompt.context.removeFile": "从上下文移除文件", - "prompt.action.attachFile": "附加文件", - "prompt.attachment.remove": "移除附件", - "prompt.action.send": "发送", - "prompt.action.stop": "停止", - "prompt.toast.pasteUnsupported.title": "不支持的附件", - "prompt.toast.pasteUnsupported.description": "此处仅能附加图片、PDF 或文本文件。", - "prompt.toast.modelAgentRequired.title": "请选择智能体和模型", - "prompt.toast.modelAgentRequired.description": "发送提示前请先选择智能体和模型。", - "prompt.toast.worktreeCreateFailed.title": "创建工作树失败", - "prompt.toast.sessionCreateFailed.title": "创建会话失败", - "prompt.toast.shellSendFailed.title": "发送 shell 命令失败", - "prompt.toast.commandSendFailed.title": "发送命令失败", - "prompt.toast.promptSendFailed.title": "发送提示失败", - "prompt.toast.promptSendFailed.description": "无法获取会话", - - "dialog.mcp.title": "MCPs", - "dialog.mcp.description": "已启用 {{enabled}} / {{total}}", - "dialog.mcp.empty": "未配置 MCPs", - - "dialog.lsp.empty": "已从文件类型自动检测到 LSPs", - - "dialog.plugins.empty": "在 opencode.json 中配置的插件", - - "mcp.status.connected": "已连接", - "mcp.status.failed": "失败", - "mcp.status.needs_auth": "需要授权", - "mcp.status.disabled": "已禁用", - - "dialog.fork.empty": "没有可用于分叉的消息", - - "dialog.directory.search.placeholder": "搜索文件夹", - "dialog.directory.empty": "未找到文件夹", - - "dialog.server.title": "服务器", - "dialog.server.description": "切换此应用连接的 Kilo 服务器。", - "dialog.server.search.placeholder": "搜索服务器", - "dialog.server.empty": "暂无服务器", - "dialog.server.add.title": "添加服务器", - "dialog.server.add.url": "服务器 URL", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "无法连接到服务器", - "dialog.server.add.checking": "检查中...", - "dialog.server.add.button": "添加服务器", - "dialog.server.add.name": "服务器名称(可选)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "用户名(可选)", - "dialog.server.add.password": "密码(可选)", - "dialog.server.edit.title": "编辑服务器", - "dialog.server.default.title": "默认服务器", - "dialog.server.default.description": "应用启动时连接此服务器,而不是启动本地服务器。需要重启。", - "dialog.server.default.none": "未选择服务器", - "dialog.server.default.set": "将当前服务器设为默认", - "dialog.server.default.clear": "清除", - "dialog.server.action.remove": "移除服务器", - "dialog.server.menu.edit": "编辑", - "dialog.server.menu.default": "设为默认", - "dialog.server.menu.defaultRemove": "取消默认", - "dialog.server.menu.delete": "删除", - "dialog.server.current": "当前服务器", - "dialog.server.status.default": "默认", - - "dialog.project.edit.title": "编辑项目", - "dialog.project.edit.name": "名称", - "dialog.project.edit.icon": "图标", - "dialog.project.edit.icon.alt": "项目图标", - "dialog.project.edit.icon.hint": "点击或拖拽图片", - "dialog.project.edit.icon.recommended": "建议:128x128px", - "dialog.project.edit.color": "颜色", - "dialog.project.edit.color.select": "选择{{color}}颜色", - "dialog.project.edit.worktree.startup": "工作区启动脚本", - "dialog.project.edit.worktree.startup.description": "在创建新的工作区 (worktree) 后运行。", - "dialog.project.edit.worktree.startup.placeholder": "例如 bun install", - - "context.breakdown.title": "上下文拆分", - "context.breakdown.note": "输入 token 的大致拆分。“其他”包含工具定义和开销。", - "context.breakdown.system": "系统", - "context.breakdown.user": "用户", - "context.breakdown.assistant": "助手", - "context.breakdown.tool": "工具调用", - "context.breakdown.other": "其他", - "context.systemPrompt.title": "系统提示词", - "context.rawMessages.title": "原始消息", - "context.stats.session": "会话", - "context.stats.messages": "消息数", - "context.stats.provider": "提供商", - "context.stats.model": "模型", - "context.stats.limit": "上下文限制", - "context.stats.totalTokens": "总 token", - "context.stats.usage": "使用率", - "context.stats.inputTokens": "输入 token", - "context.stats.outputTokens": "输出 token", - "context.stats.reasoningTokens": "推理 token", - "context.stats.cacheTokens": "缓存 token(读/写)", - "context.stats.userMessages": "用户消息", - "context.stats.assistantMessages": "助手消息", - "context.stats.totalCost": "总成本", - "context.stats.sessionCreated": "创建时间", - "context.stats.lastActivity": "最后活动", - "context.usage.tokens": "Token", - "context.usage.usage": "使用率", - "context.usage.cost": "成本", - "context.usage.clickToView": "点击查看上下文", - "context.usage.view": "查看上下文用量", - - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - - "toast.language.title": "语言", - "toast.language.description": "已切换到{{language}}", - "toast.theme.title": "主题已切换", - "toast.scheme.title": "颜色方案", - "toast.workspace.enabled.title": "工作区已启用", - "toast.workspace.enabled.description": "侧边栏现在显示多个工作树", - "toast.workspace.disabled.title": "工作区已禁用", - "toast.workspace.disabled.description": "侧边栏只显示主工作树", - "toast.permissions.autoaccept.on.title": "正在自动接受权限", - "toast.permissions.autoaccept.on.description": "权限请求将被自动批准", - "toast.permissions.autoaccept.off.title": "已停止自动接受权限", - "toast.permissions.autoaccept.off.description": "权限请求将需要批准", - "toast.model.none.title": "未选择模型", - "toast.model.none.description": "请先连接提供商以总结此会话", - "toast.file.loadFailed.title": "加载文件失败", - "toast.file.listFailed.title": "列出文件失败", - "toast.context.noLineSelection.title": "未选择行", - "toast.context.noLineSelection.description": "请先在文件标签中选择行范围。", - "toast.session.share.copyFailed.title": "无法复制链接到剪贴板", - "toast.session.share.success.title": "会话已分享", - "toast.session.share.success.description": "分享链接已复制到剪贴板", - "toast.session.share.failed.title": "分享会话失败", - "toast.session.share.failed.description": "分享会话时发生错误", - "toast.session.unshare.success.title": "已取消分享会话", - "toast.session.unshare.success.description": "会话已成功取消分享", - "toast.session.unshare.failed.title": "取消分享失败", - "toast.session.unshare.failed.description": "取消分享会话时发生错误", - "toast.session.listFailed.title": "无法加载 {{project}} 的会话", - "toast.update.title": "有可用更新", - "toast.update.description": "Kilo 有新版本 ({{version}}) 可安装。", - "toast.update.action.installRestart": "安装并重启", - "toast.update.action.notYet": "稍后", - - "error.page.title": "出了点问题", - "error.page.description": "加载应用程序时发生错误。", - "error.page.details.label": "错误详情", - "error.page.action.restart": "重启", - "error.page.action.checking": "检查中...", - "error.page.action.checkUpdates": "检查更新", - "error.page.action.updateTo": "更新到 {{version}}", - "error.page.report.prefix": "请将此错误报告给 Kilo 团队", - "error.page.report.discord": "在 Discord 上", - "error.page.version": "版本:{{version}}", - "error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html?或者 id 属性拼写错了?", - "error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?", - - "directory.error.invalidUrl": "URL 中的目录无效。", - - "error.chain.unknown": "未知错误", - "error.chain.causedBy": "原因:", - "error.chain.apiError": "API 错误", - "error.chain.status": "状态:{{status}}", - "error.chain.retryable": "可重试:{{retryable}}", - "error.chain.responseBody": "响应内容:\n{{body}}", - "error.chain.didYouMean": "你是不是想输入:{{suggestions}}", - "error.chain.modelNotFound": "未找到模型:{{provider}}/{{model}}", - "error.chain.checkConfig": "请检查你的配置 (opencode.json) 中的 provider/model 名称", - "error.chain.mcpFailed": 'MCP 服务器 "{{name}}" 启动失败。注意: Kilo 暂不支持 MCP 认证。', - "error.chain.providerAuthFailed": "提供商认证失败({{provider}}):{{message}}", - "error.chain.providerInitFailed": '无法初始化提供商 "{{provider}}"。请检查凭据和配置。', - "error.chain.configJsonInvalid": "配置文件 {{path}} 不是有效的 JSON(C)", - "error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C):{{message}}", - "error.chain.configDirectoryTypo": - '{{path}} 中的目录 "{{dir}}" 无效。请将目录重命名为 "{{suggestion}}" 或移除它。这是一个常见拼写错误。', - "error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}", - "error.chain.configInvalid": "配置文件 {{path}} 无效", - "error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效:{{message}}", - - "notification.permission.title": "需要权限", - "notification.permission.description": "{{sessionTitle}}({{projectName}})需要权限", - "notification.question.title": "问题", - "notification.question.description": "{{sessionTitle}}({{projectName}})有一个问题", - "notification.action.goToSession": "前往会话", - "notification.session.responseReady.title": "回复已就绪", - "notification.session.error.title": "会话错误", - "notification.session.error.fallbackDescription": "发生错误", - - "home.recentProjects": "最近项目", - "home.empty.title": "没有最近项目", - "home.empty.description": "通过打开本地项目开始使用", - - "session.tab.session": "会话", - "session.tab.review": "审查", - "session.tab.context": "上下文", - "session.panel.reviewAndFiles": "审查和文件", - "session.review.filesChanged": "{{count}} 个文件变更", - "session.review.change.one": "更改", - "session.review.change.other": "更改", - "session.review.loadingChanges": "正在加载更改...", - "session.review.empty": "此会话暂无更改", - "session.review.noVcs": "未检测到 Git 版本控制系统,无法显示更改", - "session.review.noSnapshot": "配置中已禁用快照跟踪,因此会话更改不可用", - "session.review.noChanges": "无更改", - "session.files.selectToOpen": "选择要打开的文件", - "session.files.all": "所有文件", - "session.files.empty": "无文件", - "session.files.binaryContent": "二进制文件(无法显示内容)", - "session.messages.renderEarlier": "显示更早的消息", - "session.messages.loadingEarlier": "正在加载更早的消息...", - "session.messages.loadEarlier": "加载更早的消息", - "session.messages.loading": "正在加载消息...", - "session.messages.jumpToLatest": "跳转到最新", - "session.context.addToContext": "将 {{selection}} 添加到上下文", - "session.todo.title": "待办事项", - "session.todo.collapse": "折叠", - "session.todo.expand": "展开", - "session.followupDock.summary.one": "{{count}} 条排队消息", - "session.followupDock.summary.other": "{{count}} 条排队消息", - "session.followupDock.sendNow": "立即发送", - "session.followupDock.edit": "编辑", - "session.followupDock.collapse": "折叠排队消息", - "session.followupDock.expand": "展开排队消息", - "session.revertDock.summary.one": "{{count}} 条已回滚消息", - "session.revertDock.summary.other": "{{count}} 条已回滚消息", - "session.revertDock.collapse": "折叠已回滚消息", - "session.revertDock.expand": "展开已回滚消息", - "session.revertDock.restore": "恢复消息", - "session.new.title": "构建任何东西", - "session.new.worktree.main": "主分支", - "session.new.worktree.mainWithBranch": "主分支({{branch}})", - "session.new.worktree.create": "创建新的 worktree", - "session.new.lastModified": "最后修改", - "session.header.search.placeholder": "搜索 {{project}}", - "session.header.searchFiles": "搜索文件", - "session.header.openIn": "打开方式", - "session.header.open.action": "打开 {{app}}", - "session.header.open.ariaLabel": "在 {{app}} 中打开", - "session.header.open.menu": "打开选项", - "session.header.open.copyPath": "复制路径", - - "status.popover.trigger": "状态", - "status.popover.ariaLabel": "服务器配置", - "status.popover.tab.servers": "服务器", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "插件", - "status.popover.action.manageServers": "管理服务器", - - "session.share.popover.title": "发布到网页", - "session.share.popover.description.shared": "此会话已在网页上公开。任何拥有链接的人都可以访问。", - "session.share.popover.description.unshared": "在网页上公开分享此会话。任何拥有链接的人都可以访问。", - "session.share.action.share": "分享", - "session.share.action.publish": "发布", - "session.share.action.publishing": "正在发布...", - "session.share.action.unpublish": "取消发布", - "session.share.action.unpublishing": "正在取消发布...", - "session.share.action.view": "查看", - "session.share.copy.copied": "已复制", - "session.share.copy.copyLink": "复制链接", - - "lsp.tooltip.none": "没有 LSP 服务器", - "lsp.label.connected": "{{count}} LSP", - - "prompt.loading": "正在加载提示...", - - "terminal.loading": "正在加载终端...", - "terminal.title": "终端", - "terminal.title.numbered": "终端 {{number}}", - "terminal.close": "关闭终端", - "terminal.connectionLost.title": "连接已丢失", - "terminal.connectionLost.description": "终端连接已中断。这可能发生在服务器重启时。", - - "common.closeTab": "关闭标签页", - "common.dismiss": "忽略", - "common.requestFailed": "请求失败", - "common.moreOptions": "更多选项", - "common.learnMore": "了解更多", - "common.rename": "重命名", - "common.reset": "重置", - "common.archive": "归档", - "common.delete": "删除", - "common.close": "关闭", - "common.edit": "编辑", - "common.loadMore": "加载更多", - "common.key.esc": "ESC", - - "sidebar.menu.toggle": "切换菜单", - "sidebar.nav.projectsAndSessions": "项目和会话", - "sidebar.settings": "设置", - "sidebar.help": "帮助", - "sidebar.workspaces.enable": "启用工作区", - "sidebar.workspaces.disable": "禁用工作区", - "sidebar.gettingStarted.title": "入门", - "sidebar.gettingStarted.line1": "Kilo 提供免费模型,你可以立即开始使用。", - "sidebar.gettingStarted.line2": "连接任意提供商即可使用更多模型,如 Claude、GPT、Gemini 等。", - "sidebar.project.recentSessions": "最近会话", - "sidebar.project.viewAllSessions": "查看全部会话", - "sidebar.project.clearNotifications": "清除通知", - - "app.name.desktop": "Kilo Desktop", - - "settings.section.desktop": "桌面", - "settings.section.server": "服务器", - - "settings.tab.general": "通用", - "settings.tab.shortcuts": "快捷键", - - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL 集成", - "settings.desktop.wsl.description": "在 Windows 的 WSL 环境中运行 Kilo 服务器。", - - "settings.general.section.appearance": "外观", - "settings.general.section.notifications": "系统通知", - "settings.general.section.updates": "更新", - "settings.general.section.sounds": "音效", - "settings.general.section.feed": "动态", - "settings.general.section.display": "显示", - "settings.general.row.language.title": "语言", - "settings.general.row.language.description": "更改 Kilo 的显示语言", - "settings.general.row.appearance.title": "外观", - "settings.general.row.appearance.description": "自定义 Kilo 在你的设备上的外观", - "settings.general.row.colorScheme.title": "配色方案", - "settings.general.row.colorScheme.description": "选择 Kilo 跟随系统、浅色或深色主题", - "settings.general.row.theme.title": "主题", - "settings.general.row.theme.description": "自定义 Kilo 的主题。", - "settings.general.row.font.title": "代码字体", - "settings.general.row.font.description": "自定义代码块使用的字体", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "界面字体", - "settings.general.row.uiFont.description": "自定义整个界面使用的字体", - "settings.general.row.followup.title": "跟进消息行为", - "settings.general.row.followup.description": "选择跟进提示是立即引导还是在队列中等待", - "settings.general.row.followup.option.queue": "排队", - "settings.general.row.followup.option.steer": "引导", - "settings.general.row.reasoningSummaries.title": "显示推理摘要", - "settings.general.row.reasoningSummaries.description": "在时间线中显示模型推理摘要", - "settings.general.row.shellToolPartsExpanded.title": "展开 shell 工具部分", - "settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分", - "settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分", - "settings.general.row.editToolPartsExpanded.description": "默认在时间线中展开 edit、write 和 patch 工具部分", - "settings.general.row.showSessionProgressBar.title": "显示会话进度条", - "settings.general.row.showSessionProgressBar.description": "当智能体正在工作时,在会话顶部显示动画进度条", - "settings.general.row.wayland.title": "使用原生 Wayland", - "settings.general.row.wayland.description": "在 Wayland 上禁用 X11 回退。需要重启。", - "settings.general.row.wayland.tooltip": "在混合刷新率显示器的 Linux 系统上,原生 Wayland 可能更稳定。", - "settings.general.row.releaseNotes.title": "发行说明", - "settings.general.row.releaseNotes.description": "更新后显示“新功能”弹窗", - - "settings.updates.row.startup.title": "启动时检查更新", - "settings.updates.row.startup.description": "在 Kilo 启动时自动检查更新", - "settings.updates.row.check.title": "检查更新", - "settings.updates.row.check.description": "手动检查更新并在有更新时安装", - "settings.updates.action.checkNow": "立即检查", - "settings.updates.action.checking": "正在检查...", - "settings.updates.toast.latest.title": "已是最新版本", - "settings.updates.toast.latest.description": "你正在使用最新版本的 Kilo。", - - "sound.option.none": "无", - "sound.option.alert01": "警报 01", - "sound.option.alert02": "警报 02", - "sound.option.alert03": "警报 03", - "sound.option.alert04": "警报 04", - "sound.option.alert05": "警报 05", - "sound.option.alert06": "警报 06", - "sound.option.alert07": "警报 07", - "sound.option.alert08": "警报 08", - "sound.option.alert09": "警报 09", - "sound.option.alert10": "警报 10", - "sound.option.bipbop01": "哔啵 01", - "sound.option.bipbop02": "哔啵 02", - "sound.option.bipbop03": "哔啵 03", - "sound.option.bipbop04": "哔啵 04", - "sound.option.bipbop05": "哔啵 05", - "sound.option.bipbop06": "哔啵 06", - "sound.option.bipbop07": "哔啵 07", - "sound.option.bipbop08": "哔啵 08", - "sound.option.bipbop09": "哔啵 09", - "sound.option.bipbop10": "哔啵 10", - "sound.option.staplebops01": "斯泰普博普斯 01", - "sound.option.staplebops02": "斯泰普博普斯 02", - "sound.option.staplebops03": "斯泰普博普斯 03", - "sound.option.staplebops04": "斯泰普博普斯 04", - "sound.option.staplebops05": "斯泰普博普斯 05", - "sound.option.staplebops06": "斯泰普博普斯 06", - "sound.option.staplebops07": "斯泰普博普斯 07", - "sound.option.nope01": "否 01", - "sound.option.nope02": "否 02", - "sound.option.nope03": "否 03", - "sound.option.nope04": "否 04", - "sound.option.nope05": "否 05", - "sound.option.nope06": "否 06", - "sound.option.nope07": "否 07", - "sound.option.nope08": "否 08", - "sound.option.nope09": "否 09", - "sound.option.nope10": "否 10", - "sound.option.nope11": "否 11", - "sound.option.nope12": "否 12", - "sound.option.yup01": "是 01", - "sound.option.yup02": "是 02", - "sound.option.yup03": "是 03", - "sound.option.yup04": "是 04", - "sound.option.yup05": "是 05", - "sound.option.yup06": "是 06", - - "settings.general.notifications.agent.title": "智能体", - "settings.general.notifications.agent.description": "当智能体完成或需要注意时显示系统通知", - "settings.general.notifications.permissions.title": "权限", - "settings.general.notifications.permissions.description": "当需要权限时显示系统通知", - "settings.general.notifications.errors.title": "错误", - "settings.general.notifications.errors.description": "发生错误时显示系统通知", - "settings.general.sounds.agent.title": "智能体", - "settings.general.sounds.agent.description": "当智能体完成或需要注意时播放声音", - "settings.general.sounds.permissions.title": "权限", - "settings.general.sounds.permissions.description": "当需要权限时播放声音", - "settings.general.sounds.errors.title": "错误", - "settings.general.sounds.errors.description": "发生错误时播放声音", - - "settings.shortcuts.title": "键盘快捷键", - "settings.shortcuts.reset.button": "重置为默认值", - "settings.shortcuts.reset.toast.title": "快捷键已重置", - "settings.shortcuts.reset.toast.description": "键盘快捷键已重置为默认设置。", - "settings.shortcuts.conflict.title": "快捷键已被占用", - "settings.shortcuts.conflict.description": "{{keybind}} 已分配给 {{titles}}。", - "settings.shortcuts.unassigned": "未设置", - "settings.shortcuts.pressKeys": "按下按键", - "settings.shortcuts.search.placeholder": "搜索快捷键", - "settings.shortcuts.search.empty": "未找到快捷键", - "settings.shortcuts.group.general": "通用", - "settings.shortcuts.group.session": "会话", - "settings.shortcuts.group.navigation": "导航", - "settings.shortcuts.group.modelAndAgent": "模型与智能体", - "settings.shortcuts.group.terminal": "终端", - "settings.shortcuts.group.prompt": "提示", - - "settings.providers.title": "提供商", - "settings.providers.description": "提供商设置将在此处可配置。", - "settings.providers.section.connected": "已连接的提供商", - "settings.providers.connected.empty": "没有已连接的提供商", - "settings.providers.section.popular": "热门提供商", - "settings.providers.tag.environment": "环境", - "settings.providers.tag.config": "配置", - "settings.providers.tag.custom": "自定义", - "settings.providers.tag.other": "其他", - - "settings.models.title": "模型", - "settings.models.description": "模型设置将在此处可配置。", - - "settings.agents.title": "智能体", - "settings.agents.description": "智能体设置将在此处可配置。", - - "settings.commands.title": "命令", - "settings.commands.description": "命令设置将在此处可配置。", - - "settings.mcp.title": "MCP", - "settings.mcp.description": "MCP 设置将在此处可配置。", - - "settings.permissions.title": "权限", - "settings.permissions.description": "控制服务器默认可以使用哪些工具。", - "settings.permissions.section.tools": "工具", - "settings.permissions.toast.updateFailed.title": "更新权限失败", - "settings.permissions.action.allow": "允许", - "settings.permissions.action.ask": "询问", - "settings.permissions.action.deny": "拒绝", - "settings.permissions.tool.read.title": "读取", - "settings.permissions.tool.read.description": "读取文件(匹配文件路径)", - "settings.permissions.tool.edit.title": "编辑", - "settings.permissions.tool.edit.description": "修改文件,包括编辑、写入、补丁和多重编辑", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "使用 glob 模式匹配文件", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "使用正则表达式搜索文件内容", - "settings.permissions.tool.list.title": "列表", - "settings.permissions.tool.list.description": "列出目录中的文件", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "运行 shell 命令", - "settings.permissions.tool.task.title": "任务", - "settings.permissions.tool.task.description": "启动子智能体", - "settings.permissions.tool.skill.title": "技能", - "settings.permissions.tool.skill.description": "按名称加载技能", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "运行语言服务器查询", - "settings.permissions.tool.todowrite.title": "更新待办", - "settings.permissions.tool.todowrite.description": "更新待办列表", - "settings.permissions.tool.webfetch.title": "网页获取", - "settings.permissions.tool.webfetch.description": "从 URL 获取内容", - "settings.permissions.tool.websearch.title": "网页搜索", - "settings.permissions.tool.websearch.description": "搜索网页", - "settings.permissions.tool.codesearch.title": "代码搜索", - "settings.permissions.tool.codesearch.description": "在网上搜索代码", - "settings.permissions.tool.external_directory.title": "外部目录", - "settings.permissions.tool.external_directory.description": "访问项目目录之外的文件", - "settings.permissions.tool.doom_loop.title": "死循环", - "settings.permissions.tool.doom_loop.description": "检测具有相同输入的重复工具调用", - - "session.delete.failed.title": "删除会话失败", - "session.delete.title": "删除会话", - "session.delete.confirm": '删除会话 "{{name}}"?', - "session.delete.button": "删除会话", - - "workspace.new": "新建工作区", - "workspace.type.local": "本地", - "workspace.type.sandbox": "沙盒", - "workspace.create.failed.title": "创建工作区失败", - "workspace.delete.failed.title": "删除工作区失败", - "workspace.resetting.title": "正在重置工作区", - "workspace.resetting.description": "这可能需要一点时间。", - "workspace.reset.failed.title": "重置工作区失败", - "workspace.reset.success.title": "工作区已重置", - "workspace.reset.success.description": "工作区已与默认分支保持一致。", - "workspace.error.stillPreparing": "工作区仍在准备中", - "workspace.status.checking": "正在检查未合并的更改...", - "workspace.status.error": "无法验证 git 状态。", - "workspace.status.clean": "未检测到未合并的更改。", - "workspace.status.dirty": "检测到未合并的更改。", - "workspace.delete.title": "删除工作区", - "workspace.delete.confirm": '删除工作区 "{{name}}"?', - "workspace.delete.button": "删除工作区", - "workspace.reset.title": "重置工作区", - "workspace.reset.confirm": '重置工作区 "{{name}}"?', - "workspace.reset.button": "重置工作区", - "workspace.reset.archived.none": "不会归档任何活跃会话。", - "workspace.reset.archived.one": "将归档 1 个会话。", - "workspace.reset.archived.many": "将归档 {{count}} 个会话。", - "workspace.reset.note": "这将把工作区重置为与默认分支一致。", - "common.open": "打开", - "dialog.releaseNotes.action.getStarted": "开始", - "dialog.releaseNotes.action.next": "下一步", - "dialog.releaseNotes.action.hideFuture": "不再显示", - "dialog.releaseNotes.media.alt": "发布预览", - "toast.project.reloadFailed.title": "无法重新加载 {{project}}", - "error.server.invalidConfiguration": "配置无效", - "common.moreCountSuffix": " (还有 {{count}} 个)", - "common.time.justNow": "刚刚", - "common.time.minutesAgo.short": "{{count}}分钟前", - "common.time.hoursAgo.short": "{{count}}小时前", - "common.time.daysAgo.short": "{{count}}天前", - "settings.providers.connected.environmentDescription": "已通过环境变量连接", - "settings.providers.custom.description": "通过基础 URL 添加与 OpenAI 兼容的提供商。", - - "app.server.unreachable": "无法连接到 {{server}}", - "app.server.retrying": "正在自动重试...", - "app.server.otherServers": "其他服务器", - "dialog.server.add.usernamePlaceholder": "用户名", - "dialog.server.add.passwordPlaceholder": "密码", - "server.row.noUsername": "无用户名", - "session.review.noVcs.createGit.title": "创建 Git 仓库", - "session.review.noVcs.createGit.description": "在此项目中跟踪、审查和撤消更改", - "session.review.noVcs.createGit.actionLoading": "正在创建 Git 仓库...", - "session.review.noVcs.createGit.action": "创建 Git 仓库", - "session.todo.progress": "已完成 {{done}} 个任务(共 {{total}} 个)", - "session.question.progress": "{{current}}/{{total}} 个问题", - "session.header.open.finder": "访达", - "session.header.open.fileExplorer": "文件资源管理器", - "session.header.open.fileManager": "文件管理器", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "终端", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "开发性能诊断", - "debugBar.na": "不适用", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": "最后一次完成的涉及会话页面的路由转换,从路由器启动到稳定后的第一次绘制。", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "过去 5 秒内的滚动帧率。", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "过去 5 秒内最差的帧时间。", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "过去 5 秒内超过 32ms 的帧。", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "过去 5 秒内的阻塞时间和长任务计数。最大任务:{{max}}。", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "过去 5 秒内观察到的最差输入延迟。", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": "过去 5 秒内的近似交互持续时间。这类似于 INP,而非官方的 Web Vitals INP。", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "当前应用生命周期的累积布局偏移。", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "使用的 JS 堆与堆限制。仅限 Chromium。", - "debugBar.mem.tip": "使用的 JS 堆与堆限制。{{used}} / {{limit}}。", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "空格", - "common.key.backspace": "退格", - "common.key.enter": "回车", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "未知", - "error.page.circular": "[循环]", - "error.globalSDK.noServerAvailable": "无可用服务器", - "error.globalSDK.serverNotAvailable": "服务器不可用", - "error.childStore.persistedCacheCreateFailed": "创建持久化缓存失败", - "error.childStore.persistedProjectMetadataCreateFailed": "创建持久化项目元数据失败", - "error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败", - "error.childStore.storeCreateFailed": "创建存储失败", - "terminal.connectionLost.abnormalClose": "WebSocket 异常关闭:{{code}}", -} satisfies Partial> diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts deleted file mode 100644 index 4e94db10abc..00000000000 --- a/packages/app/src/i18n/zht.ts +++ /dev/null @@ -1,915 +0,0 @@ -import { dict as en } from "./en" - -type Keys = keyof typeof en - -export const dict = { - "command.category.suggested": "建議", - "command.category.view": "檢視", - "command.category.project": "專案", - "command.category.provider": "提供者", - "command.category.server": "伺服器", - "command.category.session": "工作階段", - "command.category.theme": "主題", - "command.category.language": "語言", - "command.category.file": "檔案", - "command.category.context": "上下文", - "command.category.terminal": "終端機", - "command.category.model": "模型", - "command.category.mcp": "MCP", - "command.category.agent": "代理程式", - "command.category.permissions": "權限", - "command.category.workspace": "工作區", - - "command.category.settings": "設定", - "theme.scheme.system": "系統", - "theme.scheme.light": "淺色", - "theme.scheme.dark": "深色", - - "command.sidebar.toggle": "切換側邊欄", - "command.project.open": "開啟專案", - "command.provider.connect": "連接提供者", - "command.server.switch": "切換伺服器", - "command.settings.open": "開啟設定", - "command.session.previous": "上一個工作階段", - "command.session.next": "下一個工作階段", - "command.session.previous.unseen": "上一個未讀會話", - "command.session.next.unseen": "下一個未讀會話", - "command.session.archive": "封存工作階段", - - "command.palette": "命令面板", - - "command.theme.cycle": "循環主題", - "command.theme.set": "使用主題: {{theme}}", - "command.theme.scheme.cycle": "循環配色方案", - "command.theme.scheme.set": "使用配色方案: {{scheme}}", - - "command.language.cycle": "循環語言", - "command.language.set": "使用語言: {{language}}", - - "command.session.new": "新增工作階段", - "command.file.open": "開啟檔案", - "command.tab.close": "關閉分頁", - "command.context.addSelection": "將選取內容加入上下文", - "command.context.addSelection.description": "加入目前檔案中選取的行", - "command.input.focus": "聚焦輸入框", - "command.terminal.toggle": "切換終端機", - "command.fileTree.toggle": "切換檔案樹", - "command.review.toggle": "切換審查", - "command.terminal.new": "新增終端機", - "command.terminal.new.description": "建立新的終端機標籤頁", - "command.steps.toggle": "切換步驟", - "command.steps.toggle.description": "顯示或隱藏目前訊息的步驟", - "command.message.previous": "上一則訊息", - "command.message.previous.description": "跳到上一則使用者訊息", - "command.message.next": "下一則訊息", - "command.message.next.description": "跳到下一則使用者訊息", - "command.model.choose": "選擇模型", - "command.model.choose.description": "選擇不同的模型", - "command.mcp.toggle": "切換 MCP", - "command.mcp.toggle.description": "切換 MCP", - "command.agent.cycle": "循環代理程式", - "command.agent.cycle.description": "切換到下一個代理程式", - "command.agent.cycle.reverse": "反向循環代理程式", - "command.agent.cycle.reverse.description": "切換到上一個代理程式", - "command.model.variant.cycle": "循環思考強度", - "command.model.variant.cycle.description": "切換到下一個強度等級", - "command.prompt.mode.shell": "Shell", - "command.prompt.mode.normal": "Prompt", - "command.permissions.autoaccept.enable": "自動接受權限", - "command.permissions.autoaccept.disable": "停止自動接受權限", - "command.workspace.toggle": "切換工作區", - "command.workspace.toggle.description": "在側邊欄啟用或停用多個工作區", - "command.session.undo": "復原", - "command.session.undo.description": "復原上一則訊息", - "command.session.redo": "重做", - "command.session.redo.description": "重做上一則復原的訊息", - "command.session.compact": "精簡工作階段", - "command.session.compact.description": "總結工作階段以減少上下文大小", - "command.session.fork": "從訊息分支", - "command.session.fork.description": "從先前的訊息建立新工作階段", - "command.session.share": "分享工作階段", - "command.session.share.description": "分享此工作階段並將連結複製到剪貼簿", - "command.session.unshare": "取消分享工作階段", - "command.session.unshare.description": "停止分享此工作階段", - - "palette.search.placeholder": "搜尋檔案、命令和工作階段", - "palette.empty": "找不到結果", - "palette.group.commands": "命令", - "palette.group.files": "檔案", - - "dialog.provider.search.placeholder": "搜尋提供者", - "dialog.provider.empty": "找不到提供者", - "dialog.provider.group.popular": "熱門", - "dialog.provider.group.other": "其他", - "dialog.provider.tag.recommended": "推薦", - "dialog.provider.opencode.note": "精選模型,包含 Claude、GPT、Gemini 等等", - "dialog.provider.opencode.tagline": "可靠的優化模型", - "dialog.provider.opencodeGo.tagline": "適合所有人的低成本訂閱", - "dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 金鑰連線", - "dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 金鑰連線", - "dialog.provider.copilot.note": "使用 Copilot 或 API 金鑰連線", - "dialog.provider.google.note": "Gemini 模型,提供快速且結構化的回應", - "dialog.provider.openrouter.note": "從單一提供者存取所有支援的模型", - "dialog.provider.vercel.note": "透過智慧路由統一存取 AI 模型", - - "dialog.model.select.title": "選擇模型", - "dialog.model.search.placeholder": "搜尋模型", - "dialog.model.empty": "找不到模型", - "dialog.model.manage": "管理模型", - "dialog.model.manage.description": "自訂模型選擇器中顯示的模型。", - "dialog.model.manage.provider.toggle": "切換所有 {{provider}} 模型", - - "dialog.model.unpaid.freeModels.title": "Kilo 提供的免費模型", - "dialog.model.unpaid.addMore.title": "從熱門提供者新增更多模型", - - "dialog.provider.viewAll": "查看更多提供者", - - "provider.connect.title": "連線 {{provider}}", - "provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登入", - "provider.connect.selectMethod": "選擇 {{provider}} 的登入方式。", - "provider.connect.method.apiKey": "API 金鑰", - "provider.connect.status.inProgress": "正在授權...", - "provider.connect.status.waiting": "等待授權...", - "provider.connect.status.failed": "授權失敗: {{error}}", - "provider.connect.apiKey.description": - "輸入你的 {{provider}} API 金鑰以連線帳戶,並在 Kilo 中使用 {{provider}} 模型。", - "provider.connect.apiKey.label": "{{provider}} API 金鑰", - "provider.connect.apiKey.placeholder": "API 金鑰", - "provider.connect.apiKey.required": "API 金鑰為必填", - "provider.connect.opencodeZen.line1": "OpenCode Zen 為你提供一組精選的可靠最佳化模型,用於程式碼代理程式。", - "provider.connect.opencodeZen.line2": "只需一個 API 金鑰,你就能使用 Claude、GPT、Gemini、GLM 等模型。", - "provider.connect.opencodeZen.visit.prefix": "造訪 ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " 取得你的 API 金鑰。", - "provider.connect.oauth.code.visit.prefix": "造訪 ", - "provider.connect.oauth.code.visit.link": "此連結", - "provider.connect.oauth.code.visit.suffix": " 取得授權碼,以連線你的帳戶並在 Kilo 中使用 {{provider}} 模型。", - "provider.connect.oauth.code.label": "{{method}} 授權碼", - "provider.connect.oauth.code.placeholder": "授權碼", - "provider.connect.oauth.code.required": "授權碼為必填", - "provider.connect.oauth.code.invalid": "授權碼無效", - "provider.connect.oauth.auto.visit.prefix": "造訪 ", - "provider.connect.oauth.auto.visit.link": "此連結", - "provider.connect.oauth.auto.visit.suffix": " 並輸入以下程式碼,以連線你的帳戶並在 Kilo 中使用 {{provider}} 模型。", - "provider.connect.oauth.auto.confirmationCode": "確認碼", - "provider.connect.toast.connected.title": "{{provider}} 已連線", - "provider.connect.toast.connected.description": "現在可以使用 {{provider}} 模型了。", - - "provider.custom.title": "自訂提供商", - "provider.custom.description.prefix": "設定與 OpenAI 相容的提供商。請參閱", - "provider.custom.description.link": "提供商設定文件", - "provider.custom.description.suffix": "。", - "provider.custom.field.providerID.label": "提供商 ID", - "provider.custom.field.providerID.placeholder": "myprovider", - "provider.custom.field.providerID.description": "使用小寫字母、數字、連字號或底線", - "provider.custom.field.name.label": "顯示名稱", - "provider.custom.field.name.placeholder": "我的 AI 提供商", - "provider.custom.field.baseURL.label": "基礎 URL", - "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", - "provider.custom.field.apiKey.label": "API 金鑰", - "provider.custom.field.apiKey.placeholder": "API 金鑰", - "provider.custom.field.apiKey.description": "選填。若您透過標頭管理驗證,可留空。", - "provider.custom.models.label": "模型", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "model-id", - "provider.custom.models.name.label": "名稱", - "provider.custom.models.name.placeholder": "顯示名稱", - "provider.custom.models.remove": "移除模型", - "provider.custom.models.add": "新增模型", - "provider.custom.headers.label": "標頭(選填)", - "provider.custom.headers.key.label": "標頭", - "provider.custom.headers.key.placeholder": "Header-Name", - "provider.custom.headers.value.label": "值", - "provider.custom.headers.value.placeholder": "value", - "provider.custom.headers.remove": "移除標頭", - "provider.custom.headers.add": "新增標頭", - "provider.custom.error.providerID.required": "提供商 ID 為必填", - "provider.custom.error.providerID.format": "請使用小寫字母、數字、連字號或底線", - "provider.custom.error.providerID.exists": "該提供商 ID 已存在", - "provider.custom.error.name.required": "顯示名稱為必填", - "provider.custom.error.baseURL.required": "基礎 URL 為必填", - "provider.custom.error.baseURL.format": "必須以 http:// 或 https:// 開頭", - "provider.custom.error.required": "必填", - "provider.custom.error.duplicate": "重複", - - "provider.disconnect.toast.disconnected.title": "{{provider}} 已中斷連線", - "provider.disconnect.toast.disconnected.description": "{{provider}} 模型已不再可用。", - "model.tag.free": "免費", - "model.tag.latest": "最新", - - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "文字", - "model.input.image": "圖片", - "model.input.audio": "音訊", - "model.input.video": "影片", - "model.input.pdf": "pdf", - "model.tooltip.allows": "支援: {{inputs}}", - "model.tooltip.reasoning.allowed": "支援推理", - "model.tooltip.reasoning.none": "不支援推理", - "model.tooltip.context": "上下文上限 {{limit}}", - "common.search.placeholder": "搜尋", - "common.goBack": "返回", - "common.goForward": "前進", - "common.loading": "載入中", - "common.loading.ellipsis": "...", - "common.cancel": "取消", - "common.connect": "連線", - "common.disconnect": "中斷連線", - "common.continue": "提交", - "common.submit": "提交", - "common.save": "儲存", - "common.saving": "儲存中...", - "common.default": "預設", - "common.attachment": "附件", - - "prompt.placeholder.shell": "輸入 shell 命令... {{example}}", - "prompt.placeholder.normal": '隨便問點什麼... "{{example}}"', - "prompt.placeholder.simple": "隨便問點什麼...", - "prompt.placeholder.summarizeComments": "摘要評論…", - "prompt.placeholder.summarizeComment": "摘要這則評論…", - "prompt.mode.shell": "Shell", - "prompt.mode.normal": "Prompt", - "prompt.mode.shell.exit": "按 esc 退出", - - "prompt.example.1": "修復程式碼庫中的一個 TODO", - "prompt.example.2": "這個專案的技術堆疊是什麼?", - "prompt.example.3": "修復失敗的測試", - "prompt.example.4": "解釋驗證是如何運作的", - "prompt.example.5": "尋找並修復安全漏洞", - "prompt.example.6": "為使用者服務新增單元測試", - "prompt.example.7": "重構這個函式,讓它更易讀", - "prompt.example.8": "這個錯誤是什麼意思?", - "prompt.example.9": "幫我偵錯這個問題", - "prompt.example.10": "產生 API 文件", - "prompt.example.11": "最佳化資料庫查詢", - "prompt.example.12": "新增輸入驗證", - "prompt.example.13": "建立一個新的元件用於...", - "prompt.example.14": "我該如何部署這個專案?", - "prompt.example.15": "審查我的程式碼並給出最佳實務建議", - "prompt.example.16": "為這個函式新增錯誤處理", - "prompt.example.17": "解釋這個正規表示式", - "prompt.example.18": "把它轉換成 TypeScript", - "prompt.example.19": "在整個程式碼庫中新增日誌", - "prompt.example.20": "哪些相依性已經過期?", - "prompt.example.21": "幫我寫一個遷移腳本", - "prompt.example.22": "為這個端點實作快取", - "prompt.example.23": "給這個清單新增分頁", - "prompt.example.24": "建立一個 CLI 命令用於...", - "prompt.example.25": "這裡的環境變數是怎麼運作的?", - - "prompt.popover.emptyResults": "沒有符合的結果", - "prompt.popover.emptyCommands": "沒有符合的命令", - "prompt.dropzone.label": "將圖片、PDF 或文字檔案拖放到此處", - "prompt.dropzone.file.label": "拖放以 @提及檔案", - "prompt.slash.badge.custom": "自訂", - "prompt.slash.badge.skill": "技能", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "作用中", - "prompt.context.includeActiveFile": "包含作用中檔案", - "prompt.context.removeActiveFile": "從上下文移除目前檔案", - "prompt.context.removeFile": "從上下文移除檔案", - "prompt.action.attachFile": "附加檔案", - "prompt.attachment.remove": "移除附件", - "prompt.action.send": "傳送", - "prompt.action.stop": "停止", - - "prompt.toast.pasteUnsupported.title": "不支援的附件", - "prompt.toast.pasteUnsupported.description": "此處僅能附加圖片、PDF 或文字檔案。", - "prompt.toast.modelAgentRequired.title": "請選擇代理程式和模型", - "prompt.toast.modelAgentRequired.description": "傳送提示前請先選擇代理程式和模型。", - "prompt.toast.worktreeCreateFailed.title": "建立工作樹失敗", - "prompt.toast.sessionCreateFailed.title": "建立工作階段失敗", - "prompt.toast.shellSendFailed.title": "傳送 shell 命令失敗", - "prompt.toast.commandSendFailed.title": "傳送命令失敗", - "prompt.toast.promptSendFailed.title": "傳送提示失敗", - "prompt.toast.promptSendFailed.description": "無法取得工作階段", - - "dialog.mcp.title": "MCP", - "dialog.mcp.description": "已啟用 {{enabled}} / {{total}}", - "dialog.mcp.empty": "未設定 MCP", - - "dialog.lsp.empty": "已從檔案類型自動偵測到 LSPs", - "dialog.plugins.empty": "在 opencode.json 中設定的外掛程式", - - "mcp.status.connected": "已連線", - "mcp.status.failed": "失敗", - "mcp.status.needs_auth": "需要授權", - "mcp.status.disabled": "已停用", - - "dialog.fork.empty": "沒有可用於分支的訊息", - - "dialog.directory.search.placeholder": "搜尋資料夾", - "dialog.directory.empty": "找不到資料夾", - - "dialog.server.title": "伺服器", - "dialog.server.description": "切換此應用程式連線的 Kilo 伺服器。", - "dialog.server.search.placeholder": "搜尋伺服器", - "dialog.server.empty": "暫無伺服器", - "dialog.server.add.title": "新增伺服器", - "dialog.server.add.url": "伺服器 URL", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "無法連線到伺服器", - "dialog.server.add.checking": "檢查中...", - "dialog.server.add.button": "新增伺服器", - "dialog.server.add.name": "伺服器名稱(選填)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "使用者名稱(選填)", - "dialog.server.add.password": "密碼(選填)", - "dialog.server.edit.title": "編輯伺服器", - "dialog.server.default.title": "預設伺服器", - "dialog.server.default.description": "應用程式啟動時連線此伺服器,而不是啟動本地伺服器。需要重新啟動。", - "dialog.server.default.none": "未選擇伺服器", - "dialog.server.default.set": "將目前伺服器設為預設", - "dialog.server.default.clear": "清除", - "dialog.server.action.remove": "移除伺服器", - - "dialog.server.menu.edit": "編輯", - "dialog.server.menu.default": "設為預設", - "dialog.server.menu.defaultRemove": "取消預設", - "dialog.server.menu.delete": "刪除", - "dialog.server.current": "目前伺服器", - "dialog.server.status.default": "預設", - - "dialog.project.edit.title": "編輯專案", - "dialog.project.edit.name": "名稱", - "dialog.project.edit.icon": "圖示", - "dialog.project.edit.icon.alt": "專案圖示", - "dialog.project.edit.icon.hint": "點擊或拖曳圖片", - "dialog.project.edit.icon.recommended": "建議:128x128px", - "dialog.project.edit.color": "顏色", - "dialog.project.edit.color.select": "選擇{{color}}顏色", - - "dialog.project.edit.worktree.startup": "工作區啟動腳本", - "dialog.project.edit.worktree.startup.description": "在建立新的工作區 (worktree) 後執行。", - "dialog.project.edit.worktree.startup.placeholder": "例如 bun install", - "context.breakdown.title": "上下文拆分", - "context.breakdown.note": "輸入 token 的大致拆分。「其他」包含工具定義和額外開銷。", - "context.breakdown.system": "系統", - "context.breakdown.user": "使用者", - "context.breakdown.assistant": "助手", - "context.breakdown.tool": "工具呼叫", - "context.breakdown.other": "其他", - - "context.systemPrompt.title": "系統提示詞", - "context.rawMessages.title": "原始訊息", - - "context.stats.session": "工作階段", - "context.stats.messages": "訊息數", - "context.stats.provider": "提供者", - "context.stats.model": "模型", - "context.stats.limit": "上下文限制", - "context.stats.totalTokens": "總 token", - "context.stats.usage": "使用量", - "context.stats.inputTokens": "輸入 token", - "context.stats.outputTokens": "輸出 token", - "context.stats.reasoningTokens": "推理 token", - "context.stats.cacheTokens": "快取 token(讀/寫)", - "context.stats.userMessages": "使用者訊息", - "context.stats.assistantMessages": "助手訊息", - "context.stats.totalCost": "總成本", - "context.stats.sessionCreated": "建立時間", - "context.stats.lastActivity": "最後活動", - - "context.usage.tokens": "Token", - "context.usage.usage": "使用量", - "context.usage.cost": "成本", - "context.usage.clickToView": "點擊查看上下文", - "context.usage.view": "檢視上下文用量", - - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.th": "ไทย", - "language.tr": "Türkçe", - - "toast.language.title": "語言", - "toast.language.description": "已切換到 {{language}}", - - "toast.theme.title": "主題已切換", - "toast.scheme.title": "顏色方案", - - "toast.workspace.enabled.title": "工作區已啟用", - "toast.workspace.enabled.description": "側邊欄現在顯示多個工作樹", - "toast.workspace.disabled.title": "工作區已停用", - "toast.workspace.disabled.description": "側邊欄只顯示主工作樹", - - "toast.permissions.autoaccept.on.title": "正在自動接受權限", - "toast.permissions.autoaccept.on.description": "權限請求將被自動批准", - "toast.permissions.autoaccept.off.title": "已停止自動接受權限", - "toast.permissions.autoaccept.off.description": "權限請求將需要批准", - - "toast.model.none.title": "未選擇模型", - "toast.model.none.description": "請先連線提供者以總結此工作階段", - - "toast.file.loadFailed.title": "載入檔案失敗", - - "toast.file.listFailed.title": "列出檔案失敗", - "toast.context.noLineSelection.title": "未選取行", - "toast.context.noLineSelection.description": "請先在檔案分頁中選取行範圍。", - "toast.session.share.copyFailed.title": "無法複製連結到剪貼簿", - "toast.session.share.success.title": "工作階段已分享", - "toast.session.share.success.description": "分享連結已複製到剪貼簿", - "toast.session.share.failed.title": "分享工作階段失敗", - "toast.session.share.failed.description": "分享工作階段時發生錯誤", - - "toast.session.unshare.success.title": "已取消分享工作階段", - "toast.session.unshare.success.description": "工作階段已成功取消分享", - "toast.session.unshare.failed.title": "取消分享失敗", - "toast.session.unshare.failed.description": "取消分享工作階段時發生錯誤", - - "toast.session.listFailed.title": "無法載入 {{project}} 的工作階段", - - "toast.update.title": "有可用更新", - "toast.update.description": "Kilo 有新版本 ({{version}}) 可安裝。", - "toast.update.action.installRestart": "安裝並重新啟動", - "toast.update.action.notYet": "稍後", - - "error.page.title": "出了點問題", - "error.page.description": "載入應用程式時發生錯誤。", - "error.page.details.label": "錯誤詳情", - "error.page.action.restart": "重新啟動", - "error.page.action.checking": "檢查中...", - "error.page.action.checkUpdates": "檢查更新", - "error.page.action.updateTo": "更新到 {{version}}", - "error.page.report.prefix": "請將此錯誤回報給 Kilo 團隊", - "error.page.report.discord": "在 Discord 上", - "error.page.version": "版本: {{version}}", - - "error.dev.rootNotFound": "找不到根元素。你是不是忘了把它新增到 index.html? 或者 id 屬性拼錯了?", - - "error.globalSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?", - "directory.error.invalidUrl": "URL 中的目錄無效。", - - "error.chain.unknown": "未知錯誤", - "error.chain.causedBy": "原因:", - "error.chain.apiError": "API 錯誤", - "error.chain.status": "狀態: {{status}}", - "error.chain.retryable": "可重試: {{retryable}}", - "error.chain.responseBody": "回應內容:\n{{body}}", - "error.chain.didYouMean": "你是不是想輸入: {{suggestions}}", - "error.chain.modelNotFound": "找不到模型: {{provider}}/{{model}}", - "error.chain.checkConfig": "請檢查你的設定 (opencode.json) 中的 provider/model 名稱", - "error.chain.mcpFailed": 'MCP 伺服器 "{{name}}" 啟動失敗。注意: Kilo 暫不支援 MCP 認證。', - "error.chain.providerAuthFailed": "提供者認證失敗 ({{provider}}): {{message}}", - "error.chain.providerInitFailed": '無法初始化提供者 "{{provider}}"。請檢查憑證和設定。', - "error.chain.configJsonInvalid": "設定檔 {{path}} 不是有效的 JSON(C)", - "error.chain.configJsonInvalidWithMessage": "設定檔 {{path}} 不是有效的 JSON(C): {{message}}", - "error.chain.configDirectoryTypo": - '{{path}} 中的目錄 "{{dir}}" 無效。請將目錄重新命名為 "{{suggestion}}" 或移除它。這是一個常見拼寫錯誤。', - "error.chain.configFrontmatterError": "無法解析 {{path}} 中的 frontmatter:\n{{message}}", - "error.chain.configInvalid": "設定檔 {{path}} 無效", - "error.chain.configInvalidWithMessage": "設定檔 {{path}} 無效: {{message}}", - - "notification.permission.title": "需要權限", - "notification.permission.description": "{{sessionTitle}}({{projectName}})需要權限", - "notification.question.title": "問題", - "notification.question.description": "{{sessionTitle}}({{projectName}})有一個問題", - "notification.action.goToSession": "前往工作階段", - - "notification.session.responseReady.title": "回覆已就緒", - "notification.session.error.title": "工作階段錯誤", - "notification.session.error.fallbackDescription": "發生錯誤", - - "home.recentProjects": "最近專案", - "home.empty.title": "沒有最近專案", - "home.empty.description": "透過開啟本地專案開始使用", - - "session.tab.session": "工作階段", - "session.tab.review": "審查", - "session.tab.context": "上下文", - "session.panel.reviewAndFiles": "審查與檔案", - "session.review.filesChanged": "{{count}} 個檔案變更", - "session.review.change.one": "變更", - "session.review.change.other": "變更", - "session.review.loadingChanges": "正在載入變更...", - "session.review.empty": "此工作階段暫無變更", - "session.review.noChanges": "沒有變更", - "session.review.noVcs": "未偵測到 Git 版本控制系統,無法顯示變更", - "session.review.noSnapshot": "設定中已停用快照追蹤,因此無法使用工作階段變更", - "session.files.selectToOpen": "選取要開啟的檔案", - "session.files.all": "所有檔案", - "session.files.empty": "沒有檔案", - "session.files.binaryContent": "二進位檔案(無法顯示內容)", - "session.messages.renderEarlier": "顯示更早的訊息", - "session.messages.loadingEarlier": "正在載入更早的訊息...", - "session.messages.loadEarlier": "載入更早的訊息", - "session.messages.loading": "正在載入訊息...", - - "session.messages.jumpToLatest": "跳到最新", - "session.context.addToContext": "將 {{selection}} 新增到上下文", - "session.todo.title": "待辦事項", - "session.todo.collapse": "折疊", - "session.todo.expand": "展開", - "session.followupDock.summary.one": "{{count}} 則佇列訊息", - "session.followupDock.summary.other": "{{count}} 則佇列訊息", - "session.followupDock.sendNow": "立即傳送", - "session.followupDock.edit": "編輯", - "session.followupDock.collapse": "收合佇列訊息", - "session.followupDock.expand": "展開佇列訊息", - "session.revertDock.summary.one": "{{count}} 則已回復訊息", - "session.revertDock.summary.other": "{{count}} 則已回復訊息", - "session.revertDock.collapse": "收合已回復訊息", - "session.revertDock.expand": "展開已回復訊息", - "session.revertDock.restore": "還原訊息", - - "session.new.title": "建構任何東西", - "session.new.worktree.main": "主分支", - "session.new.worktree.mainWithBranch": "主分支 ({{branch}})", - "session.new.worktree.create": "建立新的 worktree", - "session.new.lastModified": "最後修改", - - "session.header.search.placeholder": "搜尋 {{project}}", - "session.header.searchFiles": "搜尋檔案", - "session.header.openIn": "開啟於", - "session.header.open.action": "開啟 {{app}}", - "session.header.open.ariaLabel": "在 {{app}} 中開啟", - "session.header.open.menu": "開啟選項", - "session.header.open.copyPath": "複製路徑", - - "status.popover.trigger": "狀態", - "status.popover.ariaLabel": "伺服器設定", - "status.popover.tab.servers": "伺服器", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "外掛程式", - "status.popover.action.manageServers": "管理伺服器", - - "session.share.popover.title": "發佈到網頁", - "session.share.popover.description.shared": "此工作階段已在網頁上公開。任何擁有連結的人都可以存取。", - "session.share.popover.description.unshared": "在網頁上公開分享此工作階段。任何擁有連結的人都可以存取。", - "session.share.action.share": "分享", - "session.share.action.publish": "發佈", - "session.share.action.publishing": "正在發佈...", - "session.share.action.unpublish": "取消發佈", - "session.share.action.unpublishing": "正在取消發佈...", - "session.share.action.view": "檢視", - "session.share.copy.copied": "已複製", - "session.share.copy.copyLink": "複製連結", - - "lsp.tooltip.none": "沒有 LSP 伺服器", - "lsp.label.connected": "{{count}} LSP", - - "prompt.loading": "正在載入提示...", - "terminal.loading": "正在載入終端機...", - "terminal.title": "終端機", - "terminal.title.numbered": "終端機 {{number}}", - "terminal.close": "關閉終端機", - - "terminal.connectionLost.title": "連線中斷", - "terminal.connectionLost.description": "終端機連線已中斷。這可能會在伺服器重新啟動時發生。", - "common.closeTab": "關閉標籤頁", - "common.dismiss": "忽略", - "common.requestFailed": "要求失敗", - "common.moreOptions": "更多選項", - "common.learnMore": "深入了解", - "common.rename": "重新命名", - "common.reset": "重設", - "common.archive": "封存", - "common.delete": "刪除", - "common.close": "關閉", - "common.edit": "編輯", - "common.loadMore": "載入更多", - - "common.key.esc": "ESC", - "sidebar.menu.toggle": "切換選單", - "sidebar.nav.projectsAndSessions": "專案與工作階段", - "sidebar.settings": "設定", - "sidebar.help": "說明", - "sidebar.workspaces.enable": "啟用工作區", - "sidebar.workspaces.disable": "停用工作區", - "sidebar.gettingStarted.title": "開始使用", - "sidebar.gettingStarted.line1": "Kilo 提供免費模型,你可以立即開始使用。", - "sidebar.gettingStarted.line2": "連線任意提供者即可使用更多模型,如 Claude、GPT、Gemini 等。", - "sidebar.project.recentSessions": "最近工作階段", - "sidebar.project.viewAllSessions": "查看全部工作階段", - "sidebar.project.clearNotifications": "清除通知", - - "app.name.desktop": "Kilo Desktop", - "settings.section.desktop": "桌面", - "settings.section.server": "伺服器", - "settings.tab.general": "一般", - "settings.tab.shortcuts": "快速鍵", - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL 整合", - "settings.desktop.wsl.description": "在 Windows 上的 WSL 中執行 Kilo 伺服器。", - - "settings.general.section.appearance": "外觀", - "settings.general.section.notifications": "系統通知", - "settings.general.section.updates": "更新", - "settings.general.section.sounds": "音效", - "settings.general.section.feed": "資訊流", - "settings.general.section.display": "顯示", - - "settings.general.row.language.title": "語言", - "settings.general.row.language.description": "變更 Kilo 的顯示語言", - "settings.general.row.appearance.title": "外觀", - "settings.general.row.appearance.description": "自訂 Kilo 在你的裝置上的外觀", - "settings.general.row.colorScheme.title": "配色方案", - "settings.general.row.colorScheme.description": "選擇 Kilo 要跟隨系統、淺色或深色主題", - "settings.general.row.theme.title": "主題", - "settings.general.row.theme.description": "自訂 Kilo 的主題。", - "settings.general.row.font.title": "程式碼字型", - "settings.general.row.font.description": "自訂程式碼區塊使用的字型", - "settings.general.row.terminalFont.title": "Terminal Font", - "settings.general.row.terminalFont.description": "Customise the font used in the terminal", - "settings.general.row.uiFont.title": "介面字型", - "settings.general.row.uiFont.description": "自訂整個介面使用的字型", - "settings.general.row.followup.title": "後續追問行為", - "settings.general.row.followup.description": "選擇後續追問提示是立即引導還是進入佇列等待", - "settings.general.row.followup.option.queue": "佇列", - "settings.general.row.followup.option.steer": "引導", - "settings.general.row.reasoningSummaries.title": "顯示推理摘要", - "settings.general.row.reasoningSummaries.description": "在時間軸中顯示模型推理摘要", - - "settings.general.row.shellToolPartsExpanded.title": "展開 shell 工具區塊", - "settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊", - "settings.general.row.editToolPartsExpanded.title": "展開 edit 工具區塊", - "settings.general.row.editToolPartsExpanded.description": "在時間軸中預設展開 edit、write 和 patch 工具區塊", - "settings.general.row.showSessionProgressBar.title": "顯示工作階段進度列", - "settings.general.row.showSessionProgressBar.description": "當代理程式正在運作時,在工作階段頂部顯示動畫進度列", - "settings.general.row.wayland.title": "使用原生 Wayland", - "settings.general.row.wayland.description": "在 Wayland 上停用 X11 後備模式。需要重新啟動。", - "settings.general.row.wayland.tooltip": "在混合更新率螢幕的 Linux 系統上,原生 Wayland 可能更穩定。", - - "settings.general.row.releaseNotes.title": "發行說明", - "settings.general.row.releaseNotes.description": "更新後顯示「新功能」彈出視窗", - - "settings.updates.row.startup.title": "啟動時檢查更新", - "settings.updates.row.startup.description": "在 Kilo 啟動時自動檢查更新", - "settings.updates.row.check.title": "檢查更新", - "settings.updates.row.check.description": "手動檢查更新並在有更新時安裝", - "settings.updates.action.checkNow": "立即檢查", - "settings.updates.action.checking": "檢查中...", - "settings.updates.toast.latest.title": "已是最新版本", - "settings.updates.toast.latest.description": "你正在使用最新版本的 Kilo。", - - "sound.option.none": "無", - "sound.option.alert01": "警報 01", - "sound.option.alert02": "警報 02", - "sound.option.alert03": "警報 03", - "sound.option.alert04": "警報 04", - "sound.option.alert05": "警報 05", - "sound.option.alert06": "警報 06", - "sound.option.alert07": "警報 07", - "sound.option.alert08": "警報 08", - "sound.option.alert09": "警報 09", - "sound.option.alert10": "警報 10", - "sound.option.bipbop01": "嗶啵 01", - "sound.option.bipbop02": "嗶啵 02", - "sound.option.bipbop03": "嗶啵 03", - "sound.option.bipbop04": "嗶啵 04", - "sound.option.bipbop05": "嗶啵 05", - "sound.option.bipbop06": "嗶啵 06", - "sound.option.bipbop07": "嗶啵 07", - "sound.option.bipbop08": "嗶啵 08", - "sound.option.bipbop09": "嗶啵 09", - "sound.option.bipbop10": "嗶啵 10", - "sound.option.staplebops01": "斯泰普博普斯 01", - "sound.option.staplebops02": "斯泰普博普斯 02", - "sound.option.staplebops03": "斯泰普博普斯 03", - "sound.option.staplebops04": "斯泰普博普斯 04", - "sound.option.staplebops05": "斯泰普博普斯 05", - "sound.option.staplebops06": "斯泰普博普斯 06", - "sound.option.staplebops07": "斯泰普博普斯 07", - "sound.option.nope01": "否 01", - "sound.option.nope02": "否 02", - "sound.option.nope03": "否 03", - "sound.option.nope04": "否 04", - "sound.option.nope05": "否 05", - "sound.option.nope06": "否 06", - "sound.option.nope07": "否 07", - "sound.option.nope08": "否 08", - "sound.option.nope09": "否 09", - "sound.option.nope10": "否 10", - "sound.option.nope11": "否 11", - "sound.option.nope12": "否 12", - "sound.option.yup01": "是 01", - "sound.option.yup02": "是 02", - "sound.option.yup03": "是 03", - "sound.option.yup04": "是 04", - "sound.option.yup05": "是 05", - "sound.option.yup06": "是 06", - "settings.general.notifications.agent.title": "代理程式", - "settings.general.notifications.agent.description": "當代理程式完成或需要注意時顯示系統通知", - "settings.general.notifications.permissions.title": "權限", - "settings.general.notifications.permissions.description": "當需要權限時顯示系統通知", - "settings.general.notifications.errors.title": "錯誤", - "settings.general.notifications.errors.description": "發生錯誤時顯示系統通知", - - "settings.general.sounds.agent.title": "代理程式", - "settings.general.sounds.agent.description": "當代理程式完成或需要注意時播放聲音", - "settings.general.sounds.permissions.title": "權限", - "settings.general.sounds.permissions.description": "當需要權限時播放聲音", - "settings.general.sounds.errors.title": "錯誤", - "settings.general.sounds.errors.description": "發生錯誤時播放聲音", - - "settings.shortcuts.title": "鍵盤快速鍵", - "settings.shortcuts.reset.button": "重設為預設值", - "settings.shortcuts.reset.toast.title": "快速鍵已重設", - "settings.shortcuts.reset.toast.description": "鍵盤快速鍵已重設為預設設定。", - "settings.shortcuts.conflict.title": "快速鍵已被占用", - "settings.shortcuts.conflict.description": "{{keybind}} 已分配給 {{titles}}。", - "settings.shortcuts.unassigned": "未設定", - "settings.shortcuts.pressKeys": "按下按鍵", - "settings.shortcuts.search.placeholder": "搜尋快速鍵", - "settings.shortcuts.search.empty": "找不到快速鍵", - - "settings.shortcuts.group.general": "一般", - "settings.shortcuts.group.session": "工作階段", - "settings.shortcuts.group.navigation": "導覽", - "settings.shortcuts.group.modelAndAgent": "模型與代理程式", - "settings.shortcuts.group.terminal": "終端機", - "settings.shortcuts.group.prompt": "提示", - - "settings.providers.title": "提供者", - "settings.providers.description": "提供者設定將在此處可設定。", - "settings.providers.section.connected": "已連線的提供商", - "settings.providers.connected.empty": "沒有已連線的提供商", - "settings.providers.section.popular": "熱門提供商", - "settings.providers.tag.environment": "環境", - "settings.providers.tag.config": "配置", - "settings.providers.tag.custom": "自訂", - "settings.providers.tag.other": "其他", - "settings.models.title": "模型", - "settings.models.description": "模型設定將在此處可設定。", - "settings.agents.title": "代理程式", - "settings.agents.description": "代理程式設定將在此處可設定。", - "settings.commands.title": "命令", - "settings.commands.description": "命令設定將在此處可設定。", - "settings.mcp.title": "MCP", - "settings.mcp.description": "MCP 設定將在此處可設定。", - - "settings.permissions.title": "權限", - "settings.permissions.description": "控制伺服器預設可以使用哪些工具。", - "settings.permissions.section.tools": "工具", - "settings.permissions.toast.updateFailed.title": "更新權限失敗", - - "settings.permissions.action.allow": "允許", - "settings.permissions.action.ask": "詢問", - "settings.permissions.action.deny": "拒絕", - - "settings.permissions.tool.read.title": "讀取", - "settings.permissions.tool.read.description": "讀取檔案(符合檔案路徑)", - "settings.permissions.tool.edit.title": "編輯", - "settings.permissions.tool.edit.description": "修改檔案,包括編輯、寫入、修補和多重編輯", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "使用 glob 模式符合檔案", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "使用正規表示式搜尋檔案內容", - "settings.permissions.tool.list.title": "清單", - "settings.permissions.tool.list.description": "列出目錄中的檔案", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "執行 shell 命令", - "settings.permissions.tool.task.title": "Task", - "settings.permissions.tool.task.description": "啟動子代理程式", - "settings.permissions.tool.skill.title": "Skill", - "settings.permissions.tool.skill.description": "按名稱載入技能", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "執行語言伺服器查詢", - "settings.permissions.tool.todowrite.title": "更新待辦", - "settings.permissions.tool.todowrite.description": "更新待辦清單", - "settings.permissions.tool.webfetch.title": "Web Fetch", - "settings.permissions.tool.webfetch.description": "從 URL 取得內容", - "settings.permissions.tool.websearch.title": "Web Search", - "settings.permissions.tool.websearch.description": "搜尋網頁", - "settings.permissions.tool.codesearch.title": "Code Search", - "settings.permissions.tool.codesearch.description": "在網路上搜尋程式碼", - "settings.permissions.tool.external_directory.title": "外部目錄", - "settings.permissions.tool.external_directory.description": "存取專案目錄之外的檔案", - "settings.permissions.tool.doom_loop.title": "Doom Loop", - "settings.permissions.tool.doom_loop.description": "偵測具有相同輸入的重複工具呼叫", - - "session.delete.failed.title": "刪除工作階段失敗", - "session.delete.title": "刪除工作階段", - "session.delete.confirm": '刪除工作階段 "{{name}}"?', - "session.delete.button": "刪除工作階段", - - "workspace.new": "新增工作區", - "workspace.type.local": "本地", - "workspace.type.sandbox": "沙盒", - "workspace.create.failed.title": "建立工作區失敗", - "workspace.delete.failed.title": "刪除工作區失敗", - "workspace.resetting.title": "正在重設工作區", - "workspace.resetting.description": "這可能需要一點時間。", - "workspace.reset.failed.title": "重設工作區失敗", - "workspace.reset.success.title": "工作區已重設", - "workspace.reset.success.description": "工作區已與預設分支保持一致。", - "workspace.error.stillPreparing": "工作區仍在準備中", - "workspace.status.checking": "正在檢查未合併的變更...", - "workspace.status.error": "無法驗證 git 狀態。", - "workspace.status.clean": "未偵測到未合併的變更。", - "workspace.status.dirty": "偵測到未合併的變更。", - "workspace.delete.title": "刪除工作區", - "workspace.delete.confirm": '刪除工作區 "{{name}}"?', - "workspace.delete.button": "刪除工作區", - "workspace.reset.title": "重設工作區", - "workspace.reset.confirm": '重設工作區 "{{name}}"?', - "workspace.reset.button": "重設工作區", - "workspace.reset.archived.none": "不會封存任何作用中工作階段。", - "workspace.reset.archived.one": "將封存 1 個工作階段。", - "workspace.reset.archived.many": "將封存 {{count}} 個工作階段。", - "workspace.reset.note": "這將把工作區重設為與預設分支一致。", - "common.open": "打開", - "dialog.releaseNotes.action.getStarted": "開始", - "dialog.releaseNotes.action.next": "下一步", - "dialog.releaseNotes.action.hideFuture": "不再顯示", - "dialog.releaseNotes.media.alt": "發佈預覽", - "toast.project.reloadFailed.title": "無法重新載入 {{project}}", - "error.server.invalidConfiguration": "無效的設定", - "common.moreCountSuffix": " (還有 {{count}} 個)", - "common.time.justNow": "剛剛", - "common.time.minutesAgo.short": "{{count}}分鐘前", - "common.time.hoursAgo.short": "{{count}}小時前", - "common.time.daysAgo.short": "{{count}}天前", - "settings.providers.connected.environmentDescription": "已從環境變數連線", - "settings.providers.custom.description": "透過基本 URL 新增與 OpenAI 相容的提供者。", - - "app.server.unreachable": "無法連線至 {{server}}", - "app.server.retrying": "正在自動重試...", - "app.server.otherServers": "其他伺服器", - "dialog.server.add.usernamePlaceholder": "使用者名稱", - "dialog.server.add.passwordPlaceholder": "密碼", - "server.row.noUsername": "無使用者名稱", - "session.review.noVcs.createGit.title": "建立 Git 儲存庫", - "session.review.noVcs.createGit.description": "追蹤、檢閱及復原此專案中的變更", - "session.review.noVcs.createGit.actionLoading": "正在建立 Git 儲存庫...", - "session.review.noVcs.createGit.action": "建立 Git 儲存庫", - "session.todo.progress": "已完成 {{done}} 個待辦事項(共 {{total}} 個)", - "session.question.progress": "{{current}}/{{total}} 個問題", - "session.header.open.finder": "Finder", - "session.header.open.fileExplorer": "檔案總管", - "session.header.open.fileManager": "檔案管理員", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "終端機", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "開發效能診斷", - "debugBar.na": "不適用", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": "最後一次完成的涉及工作階段頁面的路由轉換,從路由器啟動到穩定後的第一次繪製。", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "過去 5 秒內的滾動幀率。", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "過去 5 秒內最差的幀時間。", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "過去 5 秒內超過 32ms 的幀。", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "過去 5 秒內的阻塞時間和長任務計數。最大任務:{{max}}。", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "過去 5 秒內觀察到的最差輸入延遲。", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": "過去 5 秒內的近似互動持續時間。這類似於 INP,而非官方的 Web Vitals INP。", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "目前應用程式生命週期的累積版面配置位移。", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "使用的 JS 堆積與堆積限制。僅限 Chromium。", - "debugBar.mem.tip": "使用的 JS 堆積與堆積限制。{{used}} / {{limit}}。", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "空白鍵", - "common.key.backspace": "退格鍵", - "common.key.enter": "Enter", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "未知", - "error.page.circular": "[循環]", - "error.globalSDK.noServerAvailable": "無可用的伺服器", - "error.globalSDK.serverNotAvailable": "伺服器無法使用", - "error.childStore.persistedCacheCreateFailed": "建立持續性快取失敗", - "error.childStore.persistedProjectMetadataCreateFailed": "建立持續性專案中繼資料失敗", - "error.childStore.persistedProjectIconCreateFailed": "建立持續性專案圖示失敗", - "error.childStore.storeCreateFailed": "建立儲存區失敗", - "terminal.connectionLost.abnormalClose": "WebSocket 異常關閉:{{code}}", -} satisfies Partial> diff --git a/packages/app/src/index.css b/packages/app/src/index.css deleted file mode 100644 index 8db576dd834..00000000000 --- a/packages/app/src/index.css +++ /dev/null @@ -1,85 +0,0 @@ -@import "@opencode-ai/ui/styles/tailwind"; - -@font-face { - font-family: "JetBrainsMono Nerd Font Mono"; - src: url("/assets/JetBrainsMonoNerdFontMono-Regular.woff2") format("woff2"); - font-weight: normal; - font-style: normal; -} - -@layer components { - @keyframes session-progress-whip { - 0% { - clip-path: inset(0 100% 0 0 round 999px); - animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1); - } - - 48% { - clip-path: inset(0 0 0 0 round 999px); - animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1); - } - - 100% { - clip-path: inset(0 0 0 100% round 999px); - } - } - - [data-component="session-progress"] { - position: absolute; - inset: 0 0 auto; - height: 2px; - overflow: hidden; - pointer-events: none; - opacity: 1; - transition: opacity 220ms ease-out; - } - - [data-component="session-progress"][data-state="hiding"] { - opacity: 0; - } - - [data-component="session-progress-bar"] { - width: 100%; - height: 100%; - border-radius: 999px; - background: var(--session-progress-color); - clip-path: inset(0 100% 0 0 round 999px); - animation: session-progress-whip var(--session-progress-ms, 1800ms) infinite; - will-change: clip-path; - } - - [data-component="getting-started"] { - container-type: inline-size; - container-name: getting-started; - } - - [data-component="getting-started-actions"] { - display: flex; - flex-direction: column; - gap: 0.75rem; /* gap-3 */ - } - - [data-component="getting-started-actions"] > [data-component="button"] { - width: 100%; - } - - @container getting-started (min-width: 17rem) { - [data-component="getting-started-actions"] { - flex-direction: row; - align-items: center; - } - - [data-component="getting-started-actions"] > [data-component="button"] { - width: auto; - } - } - - @keyframes fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -} diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts deleted file mode 100644 index d80e9fffb0a..00000000000 --- a/packages/app/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { AppBaseProviders, AppInterface } from "./app" -export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker" -export { useCommand } from "./context/command" -export { loadLocaleDict, normalizeLocale, type Locale } from "./context/language" -export { type DisplayBackend, type Platform, PlatformProvider } from "./context/platform" -export { ServerConnection } from "./context/server" -export { handleNotificationClick } from "./utils/notification-click" diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx deleted file mode 100644 index 36514f56c63..00000000000 --- a/packages/app/src/pages/directory-layout.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { DataProvider } from "@opencode-ai/ui/context" -import { showToast } from "@opencode-ai/ui/toast" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { useLocation, useNavigate, useParams } from "@solidjs/router" -import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js" -import { useLanguage } from "@/context/language" -import { LocalProvider } from "@/context/local" -import { SDKProvider } from "@/context/sdk" -import { SyncProvider, useSync } from "@/context/sync" -import { decode64 } from "@/utils/base64" - -function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { - const location = useLocation() - const navigate = useNavigate() - const params = useParams() - const sync = useSync() - const slug = createMemo(() => base64Encode(props.directory)) - - createEffect(() => { - const next = sync.data.path.directory - if (!next || next === props.directory) return - const path = location.pathname.slice(slug().length + 1) - navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true }) - }) - - createResource( - () => params.id, - (id) => sync.session.sync(id), - ) - - return ( - navigate(`/${slug()}/session/${sessionID}`)} - onSessionHref={(sessionID: string) => `/${slug()}/session/${sessionID}`} - > - {props.children} - - ) -} - -export default function Layout(props: ParentProps) { - const params = useParams() - const language = useLanguage() - const navigate = useNavigate() - let invalid = "" - - const resolved = createMemo(() => { - if (!params.dir) return "" - return decode64(params.dir) ?? "" - }) - - createEffect(() => { - const dir = params.dir - if (!dir) return - if (resolved()) { - invalid = "" - return - } - if (invalid === dir) return - invalid = dir - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: language.t("directory.error.invalidUrl"), - }) - navigate("/", { replace: true }) - }) - - return ( - - {(resolved) => ( - resolved}> - - {props.children} - - - )} - - ) -} diff --git a/packages/app/src/pages/error.tsx b/packages/app/src/pages/error.tsx deleted file mode 100644 index bae8f44e548..00000000000 --- a/packages/app/src/pages/error.tsx +++ /dev/null @@ -1,318 +0,0 @@ -import { TextField } from "@opencode-ai/ui/text-field" -import { Logo } from "@opencode-ai/ui/logo" -import { Button } from "@opencode-ai/ui/button" -import { Component, Show } from "solid-js" -import { createStore } from "solid-js/store" -import { usePlatform } from "@/context/platform" -import { useLanguage } from "@/context/language" -import { Icon } from "@opencode-ai/ui/icon" - -export type InitError = { - name: string - data: Record -} - -type Translator = ReturnType["t"] -const CHAIN_SEPARATOR = "\n" + "─".repeat(40) + "\n" - -function isIssue(value: unknown): value is { message: string; path: string[] } { - if (!value || typeof value !== "object") return false - if (!("message" in value) || !("path" in value)) return false - const message = (value as { message: unknown }).message - const path = (value as { path: unknown }).path - if (typeof message !== "string") return false - if (!Array.isArray(path)) return false - return path.every((part) => typeof part === "string") -} - -function isInitError(error: unknown): error is InitError { - return ( - typeof error === "object" && - error !== null && - "name" in error && - "data" in error && - typeof (error as InitError).data === "object" - ) -} - -function safeJson(value: unknown, circular: string): string { - const seen = new WeakSet() - const json = JSON.stringify( - value, - (_key, val) => { - if (typeof val === "bigint") return val.toString() - if (typeof val === "object" && val) { - if (seen.has(val)) return circular - seen.add(val) - } - return val - }, - 2, - ) - return json ?? String(value) -} - -function formatInitError(error: InitError, t: Translator): string { - const data = error.data - const json = (value: unknown) => safeJson(value, t("error.page.circular")) - switch (error.name) { - case "MCPFailed": { - const name = typeof data.name === "string" ? data.name : "" - return t("error.chain.mcpFailed", { name }) - } - case "ProviderAuthError": { - const providerID = typeof data.providerID === "string" ? data.providerID : t("common.unknown") - const message = typeof data.message === "string" ? data.message : json(data.message) - return t("error.chain.providerAuthFailed", { provider: providerID, message }) - } - case "APIError": { - const message = typeof data.message === "string" ? data.message : t("error.chain.apiError") - const lines: string[] = [message] - - if (typeof data.statusCode === "number") { - lines.push(t("error.chain.status", { status: data.statusCode })) - } - - if (typeof data.isRetryable === "boolean") { - lines.push(t("error.chain.retryable", { retryable: data.isRetryable })) - } - - if (typeof data.responseBody === "string" && data.responseBody) { - lines.push(t("error.chain.responseBody", { body: data.responseBody })) - } - - return lines.join("\n") - } - case "ProviderModelNotFoundError": { - const { providerID, modelID, suggestions } = data as { - providerID: string - modelID: string - suggestions?: string[] - } - - const suggestionsLine = - Array.isArray(suggestions) && suggestions.length - ? [t("error.chain.didYouMean", { suggestions: suggestions.join(", ") })] - : [] - - return [ - t("error.chain.modelNotFound", { provider: providerID, model: modelID }), - ...suggestionsLine, - t("error.chain.checkConfig"), - ].join("\n") - } - case "ProviderInitError": { - const providerID = typeof data.providerID === "string" ? data.providerID : t("common.unknown") - return t("error.chain.providerInitFailed", { provider: providerID }) - } - case "ConfigJsonError": { - const path = typeof data.path === "string" ? data.path : json(data.path) - const message = typeof data.message === "string" ? data.message : "" - if (message) return t("error.chain.configJsonInvalidWithMessage", { path, message }) - return t("error.chain.configJsonInvalid", { path }) - } - case "ConfigDirectoryTypoError": { - const path = typeof data.path === "string" ? data.path : json(data.path) - const dir = typeof data.dir === "string" ? data.dir : json(data.dir) - const suggestion = typeof data.suggestion === "string" ? data.suggestion : json(data.suggestion) - return t("error.chain.configDirectoryTypo", { dir, path, suggestion }) - } - case "ConfigFrontmatterError": { - const path = typeof data.path === "string" ? data.path : json(data.path) - const message = typeof data.message === "string" ? data.message : json(data.message) - return t("error.chain.configFrontmatterError", { path, message }) - } - case "ConfigInvalidError": { - const issues = Array.isArray(data.issues) - ? data.issues.filter(isIssue).map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) - : [] - const message = typeof data.message === "string" ? data.message : "" - const path = typeof data.path === "string" ? data.path : json(data.path) - - const line = message - ? t("error.chain.configInvalidWithMessage", { path, message }) - : t("error.chain.configInvalid", { path }) - - return [line, ...issues].join("\n") - } - case "UnknownError": - return typeof data.message === "string" ? data.message : json(data) - default: - if (typeof data.message === "string") return data.message - return json(data) - } -} - -function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessage?: string): string { - const json = (value: unknown) => safeJson(value, t("error.page.circular")) - if (!error) return t("error.chain.unknown") - - if (isInitError(error)) { - const message = formatInitError(error, t) - if (depth > 0 && parentMessage === message) return "" - const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" - return indent + `${error.name}\n${message}` - } - - if (error instanceof Error) { - const isDuplicate = depth > 0 && parentMessage === error.message - const parts: string[] = [] - const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" - - const header = `${error.name}${error.message ? `: ${error.message}` : ""}` - const stack = error.stack?.trim() - - if (stack) { - const startsWithHeader = stack.startsWith(header) - - if (isDuplicate && startsWithHeader) { - const trace = stack.split("\n").slice(1).join("\n").trim() - if (trace) { - parts.push(indent + trace) - } - } - - if (isDuplicate && !startsWithHeader) { - parts.push(indent + stack) - } - - if (!isDuplicate && startsWithHeader) { - parts.push(indent + stack) - } - - if (!isDuplicate && !startsWithHeader) { - parts.push(indent + `${header}\n${stack}`) - } - } - - if (!stack && !isDuplicate) { - parts.push(indent + header) - } - - if (error.cause) { - const causeResult = formatErrorChain(error.cause, t, depth + 1, error.message) - if (causeResult) { - parts.push(causeResult) - } - } - - return parts.join("\n\n") - } - - if (typeof error === "string") { - if (depth > 0 && parentMessage === error) return "" - const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" - return indent + error - } - - const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" - return indent + json(error) -} - -function formatError(error: unknown, t: Translator): string { - return formatErrorChain(error, t, 0) -} - -interface ErrorPageProps { - error: unknown -} - -export const ErrorPage: Component = (props) => { - const platform = usePlatform() - const language = useLanguage() - const [store, setStore] = createStore({ - checking: false, - version: undefined as string | undefined, - actionError: undefined as string | undefined, - }) - - async function checkForUpdates() { - if (!platform.checkUpdate) return - setStore("checking", true) - await platform - .checkUpdate() - .then((result) => { - setStore("actionError", undefined) - if (result.updateAvailable && result.version) setStore("version", result.version) - }) - .catch((err) => { - setStore("actionError", formatError(err, language.t)) - }) - .finally(() => { - setStore("checking", false) - }) - } - - async function installUpdate() { - if (!platform.updateAndRestart) return - await platform - .updateAndRestart() - .then(() => setStore("actionError", undefined)) - .catch((err) => { - setStore("actionError", formatError(err, language.t)) - }) - } - - return ( -
-
- -
-

{language.t("error.page.title")}

-

{language.t("error.page.description")}

-
- -
- - - - {store.checking - ? language.t("error.page.action.checking") - : language.t("error.page.action.checkUpdates")} - - } - > - - - -
- - {(message) =>

{message()}

} -
-
-
- {language.t("error.page.report.prefix")} - -
- - {(version) => ( -

{language.t("error.page.version", { version: version() })}

- )} -
-
-
-
- ) -} diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx deleted file mode 100644 index 46cacdf627a..00000000000 --- a/packages/app/src/pages/home.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { createMemo, For, Match, Switch } from "solid-js" -import { Button } from "@opencode-ai/ui/button" -import { Logo } from "@opencode-ai/ui/logo" -import { useLayout } from "@/context/layout" -import { useNavigate } from "@solidjs/router" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { Icon } from "@opencode-ai/ui/icon" -import { usePlatform } from "@/context/platform" -import { DateTime } from "luxon" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { DialogSelectDirectory } from "@/components/dialog-select-directory" -import { DialogSelectServer } from "@/components/dialog-select-server" -import { useServer } from "@/context/server" -import { useGlobalSync } from "@/context/global-sync" -import { useLanguage } from "@/context/language" - -export default function Home() { - const sync = useGlobalSync() - const layout = useLayout() - const platform = usePlatform() - const dialog = useDialog() - const navigate = useNavigate() - const server = useServer() - const language = useLanguage() - const homedir = createMemo(() => sync.data.path.home) - const recent = createMemo(() => { - return sync.data.project - .slice() - .sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) - .slice(0, 5) - }) - - const serverDotClass = createMemo(() => { - const healthy = server.healthy() - if (healthy === true) return "bg-icon-success-base" - if (healthy === false) return "bg-icon-critical-base" - return "bg-border-weak-base" - }) - - function openProject(directory: string) { - layout.projects.open(directory) - server.projects.touch(directory) - navigate(`/${base64Encode(directory)}`) - } - - async function chooseProject() { - function resolve(result: string | string[] | null) { - if (Array.isArray(result)) { - for (const directory of result) { - openProject(directory) - } - } else if (result) { - openProject(result) - } - } - - if (platform.openDirectoryPickerDialog && server.isLocal()) { - const result = await platform.openDirectoryPickerDialog?.({ - title: language.t("command.project.open"), - multiple: true, - }) - resolve(result) - } else { - dialog.show( - () => , - () => resolve(null), - ) - } - } - - return ( -
- - - - 0}> -
-
-
{language.t("home.recentProjects")}
- -
-
    - - {(project) => ( - - )} - -
-
-
- -
-
{language.t("common.loading")}
- -
-
- -
- -
-
{language.t("home.empty.title")}
-
{language.t("home.empty.description")}
-
- -
-
-
-
- ) -} diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx deleted file mode 100644 index c638ec5b203..00000000000 --- a/packages/app/src/pages/layout.tsx +++ /dev/null @@ -1,2510 +0,0 @@ -import { - batch, - createEffect, - createMemo, - createResource, - For, - on, - onCleanup, - onMount, - ParentProps, - Show, - untrack, - type Accessor, -} from "solid-js" -import { makeEventListener } from "@solid-primitives/event-listener" -import { useLocation, useNavigate, useParams } from "@solidjs/router" -import { useLayout, LocalProject } from "@/context/layout" -import { useGlobalSync } from "@/context/global-sync" -import { Persist, persisted } from "@/utils/persist" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { decode64 } from "@/utils/base64" -import { ResizeHandle } from "@opencode-ai/ui/resize-handle" -import { Button } from "@opencode-ai/ui/button" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" -import { Dialog } from "@opencode-ai/ui/dialog" -import { getFilename } from "@opencode-ai/shared/util/path" -import { Session, type Message } from "@kilocode/sdk/v2/client" -import { usePlatform } from "@/context/platform" -import { useSettings } from "@/context/settings" -import { createStore, produce, reconcile } from "solid-js/store" -import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" -import type { DragEvent } from "@thisbeyond/solid-dnd" -import { useProviders } from "@/hooks/use-providers" -import { showToast, Toast, toaster } from "@opencode-ai/ui/toast" -import { useGlobalSDK } from "@/context/global-sdk" -import { clearWorkspaceTerminals } from "@/context/terminal" -import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache" -import { - clearSessionPrefetchInflight, - clearSessionPrefetch, - getSessionPrefetch, - isSessionPrefetchCurrent, - runSessionPrefetch, - setSessionPrefetch, - shouldSkipSessionPrefetch, -} from "@/context/global-sync/session-prefetch" -import { useNotification } from "@/context/notification" -import { usePermission } from "@/context/permission" -import { Binary } from "@opencode-ai/shared/util/binary" -import { retry } from "@opencode-ai/shared/util/retry" -import { playSoundById } from "@/utils/sound" -import { createAim } from "@/utils/aim" -import { setNavigate } from "@/utils/notification-click" -import { Worktree as WorktreeState } from "@/utils/worktree" -import { setSessionHandoff } from "@/pages/session/handoff" - -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context" -import { useCommand, type CommandOption } from "@/context/command" -import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd" -import { DebugBar } from "@/components/debug-bar" -import { Titlebar } from "@/components/titlebar" -import { useServer } from "@/context/server" -import { useLanguage, type Locale } from "@/context/language" -import { - displayName, - effectiveWorkspaceOrder, - errorMessage, - latestRootSession, - sortedRootSessions, - workspaceKey, -} from "./layout/helpers" -import { - collectNewSessionDeepLinks, - collectOpenProjectDeepLinks, - deepLinkEvent, - drainPendingDeepLinks, -} from "./layout/deep-links" -import { createInlineEditorController } from "./layout/inline-editor" -import { - LocalWorkspace, - SortableWorkspace, - WorkspaceDragOverlay, - type WorkspaceSidebarContext, -} from "./layout/sidebar-workspace" -import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project" -import { SidebarContent } from "./layout/sidebar-shell" - -export default function Layout(props: ParentProps) { - const [store, setStore, , ready] = persisted( - Persist.global("layout.page", ["layout.page.v1"]), - createStore({ - lastProjectSession: {} as { [directory: string]: { directory: string; id: string; at: number } }, - activeProject: undefined as string | undefined, - activeWorkspace: undefined as string | undefined, - workspaceOrder: {} as Record, - workspaceName: {} as Record, - workspaceBranchName: {} as Record>, - workspaceExpanded: {} as Record, - gettingStartedDismissed: false, - }), - ) - - const pageReady = createMemo(() => ready()) - - let scrollContainerRef: HTMLDivElement | undefined - let dialogRun = 0 - let dialogDead = false - - const params = useParams() - const globalSDK = useGlobalSDK() - const globalSync = useGlobalSync() - const layout = useLayout() - const layoutReady = createMemo(() => layout.ready()) - const platform = usePlatform() - const settings = useSettings() - const server = useServer() - const notification = useNotification() - const permission = usePermission() - const navigate = useNavigate() - setNavigate(navigate) - const providers = useProviders() - const dialog = useDialog() - const command = useCommand() - const theme = useTheme() - const language = useLanguage() - const initialDirectory = decode64(params.dir) - const location = useLocation() - const route = createMemo(() => { - const slug = params.dir - if (!slug) return { slug, dir: "" } - const dir = decode64(slug) - if (!dir) return { slug, dir: "" } - const store = globalSync.peek(dir, { bootstrap: false }) - return { - slug, - store, - dir: store[0].path.directory || dir, - } - }) - const availableThemeEntries = createMemo(() => theme.ids().map((id) => [id, theme.themes()[id]] as const)) - const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"] - const colorSchemeKey: Record = { - system: "theme.scheme.system", - light: "theme.scheme.light", - dark: "theme.scheme.dark", - } - const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme]) - const currentDir = createMemo(() => route().dir) - - const [state, setState] = createStore({ - autoselect: !initialDirectory, - busyWorkspaces: {} as Record, - hoverProject: undefined as string | undefined, - scrollSessionKey: undefined as string | undefined, - nav: undefined as HTMLElement | undefined, - sortNow: Date.now(), - sizing: false, - peek: undefined as string | undefined, - peeked: false, - }) - - const editor = createInlineEditorController() - const setBusy = (directory: string, value: boolean) => { - const key = workspaceKey(directory) - if (value) { - setState("busyWorkspaces", key, true) - return - } - setState( - "busyWorkspaces", - produce((draft) => { - delete draft[key] - }), - ) - } - const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)] - const navLeave = { current: undefined as number | undefined } - const sortNow = () => state.sortNow - let sizet: number | undefined - let sortNowInterval: ReturnType | undefined - const sortNowTimeout = setTimeout( - () => { - setState("sortNow", Date.now()) - sortNowInterval = setInterval(() => setState("sortNow", Date.now()), 60_000) - }, - 60_000 - (Date.now() % 60_000), - ) - - const aim = createAim({ - enabled: () => !layout.sidebar.opened(), - active: () => state.hoverProject, - el: () => state.nav?.querySelector("[data-component='sidebar-rail']") ?? state.nav, - onActivate: (directory) => { - globalSync.child(directory) - setState("hoverProject", directory) - }, - }) - - onCleanup(() => { - dialogDead = true - dialogRun += 1 - if (navLeave.current !== undefined) clearTimeout(navLeave.current) - clearTimeout(sortNowTimeout) - if (sortNowInterval) clearInterval(sortNowInterval) - if (sizet !== undefined) clearTimeout(sizet) - if (peekt !== undefined) clearTimeout(peekt) - aim.reset() - }) - - onMount(() => { - const stop = () => setState("sizing", false) - const blur = () => reset() - const hide = () => { - if (document.visibilityState !== "hidden") return - reset() - } - makeEventListener(window, "pointerup", stop) - makeEventListener(window, "pointercancel", stop) - makeEventListener(window, "blur", stop) - makeEventListener(window, "blur", blur) - makeEventListener(document, "visibilitychange", hide) - }) - - const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined) - const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering()) - const setHoverProject = (value: string | undefined) => { - setState("hoverProject", value) - if (value !== undefined) return - aim.reset() - } - const clearHoverProjectSoon = () => queueMicrotask(() => setHoverProject(undefined)) - - const disarm = () => { - if (navLeave.current === undefined) return - clearTimeout(navLeave.current) - navLeave.current = undefined - } - - const reset = () => { - disarm() - setHoverProject(undefined) - } - - const arm = () => { - if (layout.sidebar.opened()) return - if (state.hoverProject === undefined) return - disarm() - navLeave.current = window.setTimeout(() => { - navLeave.current = undefined - setHoverProject(undefined) - }, 300) - } - - let peekt: number | undefined - - const hoverProjectData = createMemo(() => { - const id = state.hoverProject - if (!id) return - return layout.projects.list().find((project) => project.worktree === id) - }) - - const peekProject = createMemo(() => { - const id = state.peek - if (!id) return - return layout.projects.list().find((project) => project.worktree === id) - }) - - createEffect(() => { - const p = hoverProjectData() - if (p) { - if (peekt !== undefined) { - clearTimeout(peekt) - peekt = undefined - } - setState("peek", p.worktree) - setState("peeked", true) - return - } - - setState("peeked", false) - if (state.peek === undefined) return - if (peekt !== undefined) clearTimeout(peekt) - peekt = window.setTimeout(() => { - peekt = undefined - setState("peek", undefined) - }, 180) - }) - - createEffect(() => { - if (!layout.sidebar.opened()) return - setHoverProject(undefined) - }) - - createEffect(() => { - if (!state.autoselect) return - const dir = params.dir - if (!dir) return - const directory = decode64(dir) - if (!directory) return - setState("autoselect", false) - }) - - const editorOpen = editor.editorOpen - const openEditor = editor.openEditor - const closeEditor = editor.closeEditor - const setEditor = editor.setEditor - const InlineEditor = editor.InlineEditor - - const clearSidebarHoverState = () => { - if (layout.sidebar.opened()) return - reset() - } - - const navigateWithSidebarReset = (href: string) => { - clearSidebarHoverState() - navigate(href) - layout.mobileSidebar.hide() - } - - function cycleTheme(direction = 1) { - const ids = availableThemeEntries().map(([id]) => id) - if (ids.length === 0) return - const currentIndex = ids.indexOf(theme.themeId()) - const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + ids.length) % ids.length - const nextThemeId = ids[nextIndex] - theme.setTheme(nextThemeId) - showToast({ - title: language.t("toast.theme.title"), - description: theme.name(nextThemeId), - }) - } - - function cycleColorScheme(direction = 1) { - const current = theme.colorScheme() - const currentIndex = colorSchemeOrder.indexOf(current) - const nextIndex = - currentIndex === -1 ? 0 : (currentIndex + direction + colorSchemeOrder.length) % colorSchemeOrder.length - const next = colorSchemeOrder[nextIndex] - theme.setColorScheme(next) - showToast({ - title: language.t("toast.scheme.title"), - description: colorSchemeLabel(next), - }) - } - - function setLocale(next: Locale) { - if (next === language.locale()) return - language.setLocale(next) - showToast({ - title: language.t("toast.language.title"), - description: language.t("toast.language.description", { language: language.label(next) }), - }) - } - - function cycleLanguage(direction = 1) { - const locales = language.locales - const currentIndex = locales.indexOf(language.locale()) - const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + locales.length) % locales.length - const next = locales[nextIndex] - if (!next) return - setLocale(next) - } - - const useUpdatePolling = () => - onMount(() => { - if (!platform.checkUpdate || !platform.updateAndRestart) return - - let toastId: number | undefined - let interval: ReturnType | undefined - - const pollUpdate = () => - platform.checkUpdate!().then(({ updateAvailable, version }) => { - if (!updateAvailable) return - if (toastId !== undefined) return - toastId = showToast({ - persistent: true, - icon: "download", - title: language.t("toast.update.title"), - description: language.t("toast.update.description", { version: version ?? "" }), - actions: [ - { - label: language.t("toast.update.action.installRestart"), - onClick: async () => { - await platform.updateAndRestart!() - }, - }, - { - label: language.t("toast.update.action.notYet"), - onClick: "dismiss", - }, - ], - }) - }) - - createEffect(() => { - if (!settings.ready()) return - - if (!settings.updates.startup()) { - if (interval === undefined) return - clearInterval(interval) - interval = undefined - return - } - - if (interval !== undefined) return - void pollUpdate() - interval = setInterval(pollUpdate, 10 * 60 * 1000) - }) - - onCleanup(() => { - if (interval === undefined) return - clearInterval(interval) - }) - }) - - const useSDKNotificationToasts = () => - onMount(() => { - const toastBySession = new Map() - const alertedAtBySession = new Map() - const cooldownMs = 5000 - - const dismissSessionAlert = (sessionKey: string) => { - const toastId = toastBySession.get(sessionKey) - if (toastId === undefined) return - toaster.dismiss(toastId) - toastBySession.delete(sessionKey) - alertedAtBySession.delete(sessionKey) - } - - const unsub = globalSDK.event.listen((e) => { - if (e.details?.type === "worktree.ready") { - setBusy(e.name, false) - WorktreeState.ready(e.name) - return - } - - if (e.details?.type === "worktree.failed") { - setBusy(e.name, false) - WorktreeState.failed(e.name, e.details.properties?.message ?? language.t("common.requestFailed")) - return - } - - if ( - e.details?.type === "question.replied" || - e.details?.type === "question.rejected" || - e.details?.type === "permission.replied" - ) { - const props = e.details.properties as { sessionID: string } - const sessionKey = `${e.name}:${props.sessionID}` - dismissSessionAlert(sessionKey) - return - } - - if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return - const title = - e.details.type === "permission.asked" - ? language.t("notification.permission.title") - : language.t("notification.question.title") - const icon = e.details.type === "permission.asked" ? ("checklist" as const) : ("bubble-5" as const) - const directory = e.name - const props = e.details.properties - if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return - - const [store] = globalSync.child(directory, { bootstrap: false }) - const session = store.session.find((s) => s.id === props.sessionID) - const sessionKey = `${directory}:${props.sessionID}` - - const sessionTitle = session?.title ?? language.t("command.session.new") - const projectName = getFilename(directory) - const description = - e.details.type === "permission.asked" - ? language.t("notification.permission.description", { sessionTitle, projectName }) - : language.t("notification.question.description", { sessionTitle, projectName }) - const href = `/${base64Encode(directory)}/session/${props.sessionID}` - - const now = Date.now() - const lastAlerted = alertedAtBySession.get(sessionKey) ?? 0 - if (now - lastAlerted < cooldownMs) return - alertedAtBySession.set(sessionKey, now) - - if (e.details.type === "permission.asked") { - if (settings.sounds.permissionsEnabled()) { - void playSoundById(settings.sounds.permissions()) - } - if (settings.notifications.permissions()) { - void platform.notify(title, description, href) - } - } - - if (e.details.type === "question.asked") { - if (settings.notifications.agent()) { - void platform.notify(title, description, href) - } - } - - const currentSession = params.id - if (workspaceKey(directory) === workspaceKey(currentDir()) && props.sessionID === currentSession) return - if (workspaceKey(directory) === workspaceKey(currentDir()) && session?.parentID === currentSession) return - - dismissSessionAlert(sessionKey) - - const toastId = showToast({ - persistent: true, - icon, - title, - description, - actions: [ - { - label: language.t("notification.action.goToSession"), - onClick: () => navigate(href), - }, - { - label: language.t("common.dismiss"), - onClick: "dismiss", - }, - ], - }) - toastBySession.set(sessionKey, toastId) - }) - onCleanup(unsub) - - createEffect(() => { - const currentSession = params.id - if (!currentDir() || !currentSession) return - const sessionKey = `${currentDir()}:${currentSession}` - dismissSessionAlert(sessionKey) - const [store] = globalSync.child(currentDir(), { bootstrap: false }) - const childSessions = store.session.filter((s) => s.parentID === currentSession) - for (const child of childSessions) { - dismissSessionAlert(`${currentDir()}:${child.id}`) - } - }) - }) - - useUpdatePolling() - useSDKNotificationToasts() - - function scrollToSession(sessionId: string, sessionKey: string) { - if (!scrollContainerRef) return - if (state.scrollSessionKey === sessionKey) return - const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`) - if (!element) return - const containerRect = scrollContainerRef.getBoundingClientRect() - const elementRect = element.getBoundingClientRect() - if (elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom) { - setState("scrollSessionKey", sessionKey) - return - } - setState("scrollSessionKey", sessionKey) - element.scrollIntoView({ block: "nearest", behavior: "smooth" }) - } - - const currentProject = createMemo(() => { - const directory = currentDir() - if (!directory) return - const key = workspaceKey(directory) - - const projects = layout.projects.list() - - const sandbox = projects.find((p) => p.sandboxes?.some((item) => workspaceKey(item) === key)) - if (sandbox) return sandbox - - const direct = projects.find((p) => workspaceKey(p.worktree) === key) - if (direct) return direct - - const [child] = globalSync.child(directory, { bootstrap: false }) - const id = child.project - if (!id) return - - const meta = globalSync.data.project.find((p) => p.id === id) - const root = meta?.worktree - if (!root) return - - return projects.find((p) => p.worktree === root) - }) - - const [autoselecting] = createResource(async () => { - await ready.promise - await layout.ready.promise - if (!untrack(() => state.autoselect)) return - - const list = layout.projects.list() - const last = server.projects.last() - - if (list.length === 0) { - if (!last) return - await openProject(last, true) - } else { - const next = list.find((project) => project.worktree === last) ?? list[0] - if (!next) return - await openProject(next.worktree, true) - } - }) - - const workspaceName = (directory: string, projectId?: string, branch?: string) => { - const key = workspaceKey(directory) - const direct = store.workspaceName[key] ?? store.workspaceName[directory] - if (direct) return direct - if (!projectId) return - if (!branch) return - return store.workspaceBranchName[projectId]?.[branch] - } - - const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => { - const key = workspaceKey(directory) - setStore("workspaceName", key, next) - if (!projectId) return - if (!branch) return - if (!store.workspaceBranchName[projectId]) { - setStore("workspaceBranchName", projectId, {}) - } - setStore("workspaceBranchName", projectId, branch, next) - } - - const workspaceLabel = (directory: string, branch?: string, projectId?: string) => - workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory) - - const workspaceSetting = createMemo(() => { - const project = currentProject() - if (!project) return false - if (project.vcs !== "git") return false - return layout.sidebar.workspaces(project.worktree)() - }) - - const visibleSessionDirs = createMemo(() => { - const project = currentProject() - if (!project) return [] as string[] - if (!workspaceSetting()) return [project.worktree] - - const activeDir = currentDir() - return workspaceIds(project).filter((directory) => { - const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree - const active = workspaceKey(directory) === workspaceKey(activeDir) - return expanded || active - }) - }) - - createEffect(() => { - if (!pageReady()) return - if (!layoutReady()) return - const projects = layout.projects.list() - for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) { - if (!expanded) continue - const key = workspaceKey(directory) - const project = projects.find( - (item) => - workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key), - ) - if (!project) continue - if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue - setStore("workspaceExpanded", directory, false) - } - }) - - const currentSessions = createMemo(() => { - const now = Date.now() - const dirs = visibleSessionDirs() - if (dirs.length === 0) return [] as Session[] - - const result: Session[] = [] - for (const dir of dirs) { - const [dirStore] = globalSync.child(dir, { bootstrap: true }) - const dirSessions = sortedRootSessions(dirStore, now) - result.push(...dirSessions) - } - return result - }) - - type PrefetchQueue = { - inflight: Set - pending: string[] - pendingSet: Set - running: number - } - - const prefetchChunk = 200 - const prefetchConcurrency = 2 - const prefetchPendingLimit = 10 - const span = 4 - const prefetchToken = { value: 0 } - const prefetchQueues = new Map() - - const PREFETCH_MAX_SESSIONS_PER_DIR = 10 - const prefetchedByDir = new Map>() - - const lruFor = (directory: string) => { - const existing = prefetchedByDir.get(directory) - if (existing) return existing - const created = new Set() - prefetchedByDir.set(directory, created) - return created - } - - const markPrefetched = (directory: string, sessionID: string) => { - const lru = lruFor(directory) - return pickSessionCacheEvictions({ - seen: lru, - keep: sessionID, - limit: PREFETCH_MAX_SESSIONS_PER_DIR, - preserve: params.id && workspaceKey(directory) === workspaceKey(currentDir()) ? [params.id] : undefined, - }) - } - - createEffect(() => { - const active = new Set(visibleSessionDirs()) - for (const directory of prefetchedByDir.keys()) { - if (active.has(directory)) continue - prefetchedByDir.delete(directory) - } - }) - - createEffect(() => { - route() - globalSDK.url - - prefetchToken.value += 1 - clearSessionPrefetchInflight() - prefetchQueues.clear() - }) - - createEffect(() => { - const visible = new Set(visibleSessionDirs()) - for (const [directory, q] of prefetchQueues) { - if (visible.has(directory)) continue - q.pending.length = 0 - q.pendingSet.clear() - if (q.running === 0) prefetchQueues.delete(directory) - } - }) - - const queueFor = (directory: string) => { - const existing = prefetchQueues.get(directory) - if (existing) return existing - - const created: PrefetchQueue = { - inflight: new Set(), - pending: [], - pendingSet: new Set(), - running: 0, - } - prefetchQueues.set(directory, created) - return created - } - - const mergeByID = (current: T[], incoming: T[]) => { - if (current.length === 0) { - return incoming.slice().sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) - } - - const map = new Map() - for (const item of current) { - map.set(item.id, item) - } - for (const item of incoming) { - map.set(item.id, item) - } - return [...map.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) - } - - async function prefetchMessages(directory: string, sessionID: string, token: number) { - const [store, setStore] = globalSync.child(directory, { bootstrap: false }) - - return runSessionPrefetch({ - directory, - sessionID, - task: (rev) => - retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk })) - .then((messages) => { - if (prefetchToken.value !== token) return - if (!isSessionPrefetchCurrent(directory, sessionID, rev)) return - - const items = (messages.data ?? []).filter((x) => !!x?.info?.id) - const next = items.map((x) => x.info).filter((m): m is Message => !!m?.id) - const sorted = mergeByID([], next) - const stale = markPrefetched(directory, sessionID) - const cursor = messages.response.headers.get("x-next-cursor") ?? undefined - const meta = { - limit: sorted.length, - cursor, - complete: !cursor, - at: Date.now(), - } - - if (stale.length > 0) { - clearSessionPrefetch(directory, stale) - for (const id of stale) { - globalSync.todo.set(id, undefined) - } - } - - const current = store.message[sessionID] ?? [] - const merged = mergeByID( - current.filter((item): item is Message => !!item?.id), - sorted, - ) - - if (!isSessionPrefetchCurrent(directory, sessionID, rev)) return - - batch(() => { - if (stale.length > 0) { - setStore( - produce((draft) => { - dropSessionCaches(draft, stale) - }), - ) - } - - setStore("message", sessionID, reconcile(merged, { key: "id" })) - setSessionPrefetch({ directory, sessionID, ...meta }) - - for (const message of items) { - const currentParts = store.part[message.info.id] ?? [] - const mergedParts = mergeByID( - currentParts.filter((item): item is (typeof currentParts)[number] & { id: string } => !!item?.id), - message.parts.filter((item): item is (typeof message.parts)[number] & { id: string } => !!item?.id), - ) - - setStore("part", message.info.id, reconcile(mergedParts, { key: "id" })) - } - }) - - return meta - }) - .catch(() => undefined), - }) - } - - const pumpPrefetch = (directory: string) => { - const q = queueFor(directory) - if (q.running >= prefetchConcurrency) return - - const sessionID = q.pending.shift() - if (!sessionID) return - - q.pendingSet.delete(sessionID) - q.inflight.add(sessionID) - q.running += 1 - - const token = prefetchToken.value - - void prefetchMessages(directory, sessionID, token).finally(() => { - q.running -= 1 - q.inflight.delete(sessionID) - pumpPrefetch(directory) - }) - } - - const prefetchSession = (session: Session, priority: "high" | "low" = "low") => { - const directory = session.directory - if (!directory) return - - const [store] = globalSync.child(directory, { bootstrap: false }) - const cached = untrack(() => { - const info = getSessionPrefetch(directory, session.id) - return shouldSkipSessionPrefetch({ - message: store.message[session.id] !== undefined, - info, - chunk: prefetchChunk, - }) - }) - if (cached) return - - const q = queueFor(directory) - if (q.inflight.has(session.id)) return - if (q.pendingSet.has(session.id)) { - if (priority !== "high") return - const index = q.pending.indexOf(session.id) - if (index > 0) { - q.pending.splice(index, 1) - q.pending.unshift(session.id) - } - return - } - - const lru = lruFor(directory) - const known = lru.has(session.id) - if (!known && lru.size >= PREFETCH_MAX_SESSIONS_PER_DIR && priority !== "high") return - - if (priority === "high") q.pending.unshift(session.id) - if (priority !== "high") q.pending.push(session.id) - q.pendingSet.add(session.id) - - while (q.pending.length > prefetchPendingLimit) { - const dropped = q.pending.pop() - if (!dropped) continue - q.pendingSet.delete(dropped) - } - - pumpPrefetch(directory) - } - - const warm = (sessions: Session[], index: number) => { - for (let offset = 1; offset <= span; offset++) { - const next = sessions[index + offset] - if (next) prefetchSession(next, offset === 1 ? "high" : "low") - - const prev = sessions[index - offset] - if (prev) prefetchSession(prev, offset === 1 ? "high" : "low") - } - } - - createEffect(() => { - const sessions = currentSessions() - if (sessions.length === 0) return - - const index = params.id ? sessions.findIndex((s) => s.id === params.id) : 0 - if (index === -1) return - - if (!params.id) { - const first = sessions[index] - if (first) prefetchSession(first, "high") - } - - warm(sessions, index) - }) - - function navigateSessionByOffset(offset: number) { - const sessions = currentSessions() - if (sessions.length === 0) return - - const sessionIndex = params.id ? sessions.findIndex((s) => s.id === params.id) : -1 - - let targetIndex: number - if (sessionIndex === -1) { - targetIndex = offset > 0 ? 0 : sessions.length - 1 - } else { - targetIndex = (sessionIndex + offset + sessions.length) % sessions.length - } - - const session = sessions[targetIndex] - if (!session) return - - prefetchSession(session, "high") - warm(sessions, targetIndex) - - navigateToSession(session) - } - - function navigateProjectByOffset(offset: number) { - const projects = layout.projects.list() - if (projects.length === 0) return - - const current = currentProject()?.worktree - const fallback = currentDir() ? projectRoot(currentDir()) : undefined - const active = current ?? fallback - const index = active ? projects.findIndex((project) => project.worktree === active) : -1 - - const target = - index === -1 - ? offset > 0 - ? projects[0] - : projects[projects.length - 1] - : projects[(index + offset + projects.length) % projects.length] - if (!target) return - - // warm up child store to prevent flicker - globalSync.child(target.worktree) - void openProject(target.worktree) - } - - function navigateSessionByUnseen(offset: number) { - const sessions = currentSessions() - if (sessions.length === 0) return - - const hasUnseen = sessions.some((session) => notification.session.unseenCount(session.id) > 0) - if (!hasUnseen) return - - const activeIndex = params.id ? sessions.findIndex((s) => s.id === params.id) : -1 - const start = activeIndex === -1 ? (offset > 0 ? -1 : 0) : activeIndex - - for (let i = 1; i <= sessions.length; i++) { - const index = offset > 0 ? (start + i) % sessions.length : (start - i + sessions.length) % sessions.length - const session = sessions[index] - if (!session) continue - if (notification.session.unseenCount(session.id) === 0) continue - - prefetchSession(session, "high") - warm(sessions, index) - - navigateToSession(session) - return - } - } - - async function archiveSession(session: Session) { - const [store, setStore] = globalSync.child(session.directory) - const sessions = store.session ?? [] - const index = sessions.findIndex((s) => s.id === session.id) - const nextSession = sessions[index + 1] ?? sessions[index - 1] - - await globalSDK.client.session.update({ - directory: session.directory, - sessionID: session.id, - time: { archived: Date.now() }, - }) - setStore( - produce((draft) => { - const match = Binary.search(draft.session, session.id, (s) => s.id) - if (match.found) draft.session.splice(match.index, 1) - }), - ) - if (session.id === params.id) { - if (nextSession) { - navigate(`/${params.dir}/session/${nextSession.id}`) - } else { - navigate(`/${params.dir}/session`) - } - } - } - - command.register("layout", () => { - const commands: CommandOption[] = [ - { - id: "sidebar.toggle", - title: language.t("command.sidebar.toggle"), - category: language.t("command.category.view"), - keybind: "mod+b", - onSelect: () => layout.sidebar.toggle(), - }, - { - id: "project.open", - title: language.t("command.project.open"), - category: language.t("command.category.project"), - keybind: "mod+o", - onSelect: () => chooseProject(), - }, - { - id: "project.previous", - title: language.t("command.project.previous"), - category: language.t("command.category.project"), - keybind: "mod+alt+arrowup", - onSelect: () => navigateProjectByOffset(-1), - }, - { - id: "project.next", - title: language.t("command.project.next"), - category: language.t("command.category.project"), - keybind: "mod+alt+arrowdown", - onSelect: () => navigateProjectByOffset(1), - }, - { - id: "provider.connect", - title: language.t("command.provider.connect"), - category: language.t("command.category.provider"), - onSelect: () => connectProvider(), - }, - { - id: "server.switch", - title: language.t("command.server.switch"), - category: language.t("command.category.server"), - onSelect: () => openServer(), - }, - { - id: "settings.open", - title: language.t("command.settings.open"), - category: language.t("command.category.settings"), - keybind: "mod+comma", - onSelect: () => openSettings(), - }, - { - id: "session.previous", - title: language.t("command.session.previous"), - category: language.t("command.category.session"), - keybind: "alt+arrowup", - onSelect: () => navigateSessionByOffset(-1), - }, - { - id: "session.next", - title: language.t("command.session.next"), - category: language.t("command.category.session"), - keybind: "alt+arrowdown", - onSelect: () => navigateSessionByOffset(1), - }, - { - id: "session.previous.unseen", - title: language.t("command.session.previous.unseen"), - category: language.t("command.category.session"), - keybind: "shift+alt+arrowup", - onSelect: () => navigateSessionByUnseen(-1), - }, - { - id: "session.next.unseen", - title: language.t("command.session.next.unseen"), - category: language.t("command.category.session"), - keybind: "shift+alt+arrowdown", - onSelect: () => navigateSessionByUnseen(1), - }, - { - id: "session.archive", - title: language.t("command.session.archive"), - category: language.t("command.category.session"), - keybind: "mod+shift+backspace", - disabled: !params.dir || !params.id, - onSelect: () => { - const session = currentSessions().find((s) => s.id === params.id) - if (session) void archiveSession(session) - }, - }, - { - id: "workspace.new", - title: language.t("workspace.new"), - category: language.t("command.category.workspace"), - keybind: "mod+shift+w", - disabled: !workspaceSetting(), - onSelect: () => { - const project = currentProject() - if (!project) return - return createWorkspace(project) - }, - }, - { - id: "workspace.toggle", - title: language.t("command.workspace.toggle"), - description: language.t("command.workspace.toggle.description"), - category: language.t("command.category.workspace"), - slash: "workspace", - disabled: !currentProject() || currentProject()?.vcs !== "git", - onSelect: () => { - const project = currentProject() - if (!project) return - if (project.vcs !== "git") return - const wasEnabled = layout.sidebar.workspaces(project.worktree)() - layout.sidebar.toggleWorkspaces(project.worktree) - showToast({ - title: wasEnabled - ? language.t("toast.workspace.disabled.title") - : language.t("toast.workspace.enabled.title"), - description: wasEnabled - ? language.t("toast.workspace.disabled.description") - : language.t("toast.workspace.enabled.description"), - }) - }, - }, - { - id: "theme.cycle", - title: language.t("command.theme.cycle"), - category: language.t("command.category.theme"), - keybind: "mod+shift+t", - onSelect: () => cycleTheme(1), - }, - ] - - for (const [id] of availableThemeEntries()) { - commands.push({ - id: `theme.set.${id}`, - title: language.t("command.theme.set", { theme: theme.name(id) }), - category: language.t("command.category.theme"), - onSelect: () => theme.commitPreview(), - onHighlight: () => { - theme.previewTheme(id) - return () => theme.cancelPreview() - }, - }) - } - - commands.push({ - id: "theme.scheme.cycle", - title: language.t("command.theme.scheme.cycle"), - category: language.t("command.category.theme"), - keybind: "mod+shift+s", - onSelect: () => cycleColorScheme(1), - }) - - for (const scheme of colorSchemeOrder) { - commands.push({ - id: `theme.scheme.${scheme}`, - title: language.t("command.theme.scheme.set", { scheme: colorSchemeLabel(scheme) }), - category: language.t("command.category.theme"), - onSelect: () => theme.commitPreview(), - onHighlight: () => { - theme.previewColorScheme(scheme) - return () => theme.cancelPreview() - }, - }) - } - - commands.push({ - id: "language.cycle", - title: language.t("command.language.cycle"), - category: language.t("command.category.language"), - onSelect: () => cycleLanguage(1), - }) - - for (const locale of language.locales) { - commands.push({ - id: `language.set.${locale}`, - title: language.t("command.language.set", { language: language.label(locale) }), - category: language.t("command.category.language"), - onSelect: () => setLocale(locale), - }) - } - - return commands - }) - - function connectProvider() { - const run = ++dialogRun - void import("@/components/dialog-select-provider").then((x) => { - if (dialogDead || dialogRun !== run) return - dialog.show(() => ) - }) - } - - function openServer() { - const run = ++dialogRun - void import("@/components/dialog-select-server").then((x) => { - if (dialogDead || dialogRun !== run) return - dialog.show(() => ) - }) - } - - function openSettings() { - const run = ++dialogRun - void import("@/components/dialog-settings").then((x) => { - if (dialogDead || dialogRun !== run) return - dialog.show(() => ) - }) - } - - function projectRoot(directory: string) { - const key = workspaceKey(directory) - const project = layout.projects - .list() - .find( - (item) => - workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key), - ) - if (project) return project.worktree - - const known = Object.entries(store.workspaceOrder).find( - ([root, dirs]) => workspaceKey(root) === key || dirs.some((item) => workspaceKey(item) === key), - ) - if (known) return known[0] - - const [child] = globalSync.child(directory, { bootstrap: false }) - const id = child.project - if (!id) return directory - - const meta = globalSync.data.project.find((item) => item.id === id) - return meta?.worktree ?? directory - } - - function activeProjectRoot(directory: string) { - return currentProject()?.worktree ?? projectRoot(directory) - } - - function rememberSessionRoute(directory: string, id: string, root = activeProjectRoot(directory)) { - setStore("lastProjectSession", root, { directory, id, at: Date.now() }) - return root - } - - function clearLastProjectSession(root: string) { - if (!store.lastProjectSession[root]) return - setStore( - "lastProjectSession", - produce((draft) => { - delete draft[root] - }), - ) - } - - function syncSessionRoute(directory: string, id: string, root = activeProjectRoot(directory)) { - rememberSessionRoute(directory, id, root) - notification.session.markViewed(id) - const expanded = untrack(() => store.workspaceExpanded[directory]) - if (expanded === false) { - setStore("workspaceExpanded", directory, true) - } - requestAnimationFrame(() => scrollToSession(id, `${directory}:${id}`)) - return root - } - - async function navigateToProject(directory: string | undefined) { - if (!directory) return - const root = projectRoot(directory) - server.projects.touch(root) - const project = layout.projects.list().find((item) => item.worktree === root) - let dirs = project - ? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root]) - : [root] - const canOpen = (value: string | undefined) => { - if (!value) return false - return dirs.some((item) => workspaceKey(item) === workspaceKey(value)) - } - const refreshDirs = async (target?: string) => { - if (!target || target === root || canOpen(target)) return canOpen(target) - const listed = await globalSDK.client.worktree - .list({ directory: root }) - .then((x) => x.data ?? []) - .catch(() => [] as string[]) - dirs = effectiveWorkspaceOrder(root, [root, ...listed], store.workspaceOrder[root]) - return canOpen(target) - } - const openSession = async (target: { directory: string; id: string }) => { - if (!canOpen(target.directory)) return false - const [data] = globalSync.child(target.directory, { bootstrap: false }) - if (data.session.some((item) => item.id === target.id)) { - setStore("lastProjectSession", root, { directory: target.directory, id: target.id, at: Date.now() }) - navigateWithSidebarReset(`/${base64Encode(target.directory)}/session/${target.id}`) - return true - } - const resolved = await globalSDK.client.session - .get({ sessionID: target.id }) - .then((x) => x.data) - .catch(() => undefined) - if (!resolved?.directory) return false - if (!canOpen(resolved.directory)) return false - setStore("lastProjectSession", root, { directory: resolved.directory, id: resolved.id, at: Date.now() }) - navigateWithSidebarReset(`/${base64Encode(resolved.directory)}/session/${resolved.id}`) - return true - } - - const projectSession = store.lastProjectSession[root] - if (projectSession?.id) { - await refreshDirs(projectSession.directory) - const opened = await openSession(projectSession) - if (opened) return - clearLastProjectSession(root) - } - - const latest = latestRootSession( - dirs.map((item) => globalSync.child(item, { bootstrap: false })[0]), - Date.now(), - ) - if (latest && (await openSession(latest))) { - return - } - - const fetched = latestRootSession( - await Promise.all( - dirs.map(async (item) => ({ - path: { directory: item }, - session: await globalSDK.client.session - .list({ directory: item }) - .then((x) => x.data ?? []) - .catch(() => []), - })), - ), - Date.now(), - ) - if (fetched && (await openSession(fetched))) { - return - } - - navigateWithSidebarReset(`/${base64Encode(root)}/session`) - } - - function navigateToSession(session: Session | undefined) { - if (!session) return - navigateWithSidebarReset(`/${base64Encode(session.directory)}/session/${session.id}`) - } - - function openProject(directory: string, navigate = true) { - layout.projects.open(directory) - if (navigate) return navigateToProject(directory) - } - - const handleDeepLinks = (urls: string[]) => { - if (!server.isLocal()) return - - for (const directory of collectOpenProjectDeepLinks(urls)) { - void openProject(directory) - } - - for (const link of collectNewSessionDeepLinks(urls)) { - void openProject(link.directory, false) - const slug = base64Encode(link.directory) - if (link.prompt) { - setSessionHandoff(slug, { prompt: link.prompt }) - } - const href = link.prompt ? `/${slug}/session?prompt=${encodeURIComponent(link.prompt)}` : `/${slug}/session` - navigateWithSidebarReset(href) - } - } - - onMount(() => { - const handler = (event: Event) => { - const detail = (event as CustomEvent<{ urls: string[] }>).detail - const urls = detail?.urls ?? [] - if (urls.length === 0) return - handleDeepLinks(urls) - } - - handleDeepLinks(drainPendingDeepLinks(window)) - makeEventListener(window, deepLinkEvent, handler as EventListener) - }) - - async function renameProject(project: LocalProject, next: string) { - const current = displayName(project) - if (next === current) return - const name = next === getFilename(project.worktree) ? "" : next - - if (project.id && project.id !== "global") { - await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name }) - return - } - - globalSync.project.meta(project.worktree, { name }) - } - - const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => { - const current = workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory) - if (current === next) return - setWorkspaceName(directory, next, projectId, branch) - } - - function closeProject(directory: string) { - const list = layout.projects.list() - const key = workspaceKey(directory) - const index = list.findIndex((x) => workspaceKey(x.worktree) === key) - const active = workspaceKey(currentProject()?.worktree ?? "") === key - if (index === -1) return - const next = list[index + 1] - - if (!active) { - layout.projects.close(directory) - return - } - - if (!next) { - layout.projects.close(directory) - navigate("/") - return - } - - navigateWithSidebarReset(`/${base64Encode(next.worktree)}/session`) - layout.projects.close(directory) - queueMicrotask(() => { - void navigateToProject(next.worktree) - }) - } - - function toggleProjectWorkspaces(project: LocalProject) { - const enabled = layout.sidebar.workspaces(project.worktree)() - if (enabled) { - layout.sidebar.toggleWorkspaces(project.worktree) - return - } - if (project.vcs !== "git") return - layout.sidebar.toggleWorkspaces(project.worktree) - } - - const showEditProjectDialog = (project: LocalProject) => { - const run = ++dialogRun - void import("@/components/dialog-edit-project").then((x) => { - if (dialogDead || dialogRun !== run) return - dialog.show(() => ) - }) - } - - async function chooseProject() { - function resolve(result: string | string[] | null) { - if (Array.isArray(result)) { - for (const directory of result) { - void openProject(directory, false) - } - void navigateToProject(result[0]) - } else if (result) { - void openProject(result) - } - } - - if (platform.openDirectoryPickerDialog && server.isLocal()) { - const result = await platform.openDirectoryPickerDialog?.({ - title: language.t("command.project.open"), - multiple: true, - }) - resolve(result) - } else { - const run = ++dialogRun - void import("@/components/dialog-select-directory").then((x) => { - if (dialogDead || dialogRun !== run) return - dialog.show( - () => , - () => resolve(null), - ) - }) - } - } - - const deleteWorkspace = async (root: string, directory: string, leaveDeletedWorkspace = false) => { - if (directory === root) return - - const current = currentDir() - const currentKey = workspaceKey(current) - const deletedKey = workspaceKey(directory) - const shouldLeave = leaveDeletedWorkspace || (!!params.dir && currentKey === deletedKey) - if (!leaveDeletedWorkspace && shouldLeave) { - navigateWithSidebarReset(`/${base64Encode(root)}/session`) - } - - setBusy(directory, true) - - const result = await globalSDK.client.worktree - .remove({ directory: root, worktreeRemoveInput: { directory } }) - .then((x) => x.data) - .catch((err) => { - showToast({ - title: language.t("workspace.delete.failed.title"), - description: errorMessage(err, language.t("common.requestFailed")), - }) - return false - }) - - setBusy(directory, false) - - if (!result) return - - if (workspaceKey(store.lastProjectSession[root]?.directory ?? "") === workspaceKey(directory)) { - clearLastProjectSession(root) - } - - globalSync.set( - "project", - produce((draft) => { - const project = draft.find((item) => item.worktree === root) - if (!project) return - project.sandboxes = (project.sandboxes ?? []).filter((sandbox) => sandbox !== directory) - }), - ) - setStore("workspaceOrder", root, (order) => (order ?? []).filter((workspace) => workspace !== directory)) - - layout.projects.close(directory) - layout.projects.open(root) - - if (shouldLeave) return - - const nextCurrent = currentDir() - const nextKey = workspaceKey(nextCurrent) - const project = layout.projects.list().find((item) => item.worktree === root) - const dirs = project - ? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root]) - : [root] - const valid = dirs.some((item) => workspaceKey(item) === nextKey) - - if (params.dir && projectRoot(nextCurrent) === root && !valid) { - navigateWithSidebarReset(`/${base64Encode(root)}/session`) - } - } - - const resetWorkspace = async (root: string, directory: string) => { - if (directory === root) return - setBusy(directory, true) - - const progress = showToast({ - persistent: true, - title: language.t("workspace.resetting.title"), - description: language.t("workspace.resetting.description"), - }) - const dismiss = () => toaster.dismiss(progress) - - const sessions: Session[] = await globalSDK.client.session - .list({ directory }) - .then((x) => x.data ?? []) - .catch(() => []) - - clearWorkspaceTerminals( - directory, - sessions.map((s) => s.id), - platform, - ) - await globalSDK.client.instance.dispose({ directory }).catch(() => undefined) - - const result = await globalSDK.client.worktree - .reset({ directory: root, worktreeResetInput: { directory } }) - .then((x) => x.data) - .catch((err) => { - showToast({ - title: language.t("workspace.reset.failed.title"), - description: errorMessage(err, language.t("common.requestFailed")), - }) - return false - }) - - if (!result) { - setBusy(directory, false) - dismiss() - return - } - - const archivedAt = Date.now() - await Promise.all( - sessions - .filter((session) => session.time.archived === undefined) - .map((session) => - globalSDK.client.session - .update({ - sessionID: session.id, - directory: session.directory, - time: { archived: archivedAt }, - }) - .catch(() => undefined), - ), - ) - - setBusy(directory, false) - dismiss() - - showToast({ - title: language.t("workspace.reset.success.title"), - description: language.t("workspace.reset.success.description"), - actions: [ - { - label: language.t("command.session.new"), - onClick: () => { - const href = `/${base64Encode(directory)}/session` - navigate(href) - layout.mobileSidebar.hide() - }, - }, - { - label: language.t("common.dismiss"), - onClick: "dismiss", - }, - ], - }) - } - - function DialogDeleteWorkspace(props: { root: string; directory: string }) { - const name = createMemo(() => getFilename(props.directory)) - const [data, setData] = createStore({ - status: "loading" as "loading" | "ready" | "error", - dirty: false, - }) - - onMount(() => { - globalSDK.client.file - .status({ directory: props.directory }) - .then((x) => { - const files = x.data ?? [] - const dirty = files.length > 0 - setData({ status: "ready", dirty }) - }) - .catch(() => { - setData({ status: "error", dirty: false }) - }) - }) - - const handleDelete = () => { - const leaveDeletedWorkspace = !!params.dir && workspaceKey(currentDir()) === workspaceKey(props.directory) - if (leaveDeletedWorkspace) { - navigateWithSidebarReset(`/${base64Encode(props.root)}/session`) - } - dialog.close() - void deleteWorkspace(props.root, props.directory, leaveDeletedWorkspace) - } - - const description = () => { - if (data.status === "loading") return language.t("workspace.status.checking") - if (data.status === "error") return language.t("workspace.status.error") - if (!data.dirty) return language.t("workspace.status.clean") - return language.t("workspace.status.dirty") - } - - return ( - -
-
- - {language.t("workspace.delete.confirm", { name: name() })} - - {description()} -
-
- - -
-
-
- ) - } - - function DialogResetWorkspace(props: { root: string; directory: string }) { - const name = createMemo(() => getFilename(props.directory)) - const [state, setState] = createStore({ - status: "loading" as "loading" | "ready" | "error", - dirty: false, - sessions: [] as Session[], - }) - - const refresh = async () => { - const sessions = await globalSDK.client.session - .list({ directory: props.directory }) - .then((x) => x.data ?? []) - .catch(() => []) - const active = sessions.filter((session) => session.time.archived === undefined) - setState({ sessions: active }) - } - - onMount(() => { - globalSDK.client.file - .status({ directory: props.directory }) - .then((x) => { - const files = x.data ?? [] - const dirty = files.length > 0 - setState({ status: "ready", dirty }) - void refresh() - }) - .catch(() => { - setState({ status: "error", dirty: false }) - }) - }) - - const handleReset = () => { - dialog.close() - void resetWorkspace(props.root, props.directory) - } - - const archivedCount = () => state.sessions.length - - const description = () => { - if (state.status === "loading") return language.t("workspace.status.checking") - if (state.status === "error") return language.t("workspace.status.error") - if (!state.dirty) return language.t("workspace.status.clean") - return language.t("workspace.status.dirty") - } - - const archivedLabel = () => { - const count = archivedCount() - if (count === 0) return language.t("workspace.reset.archived.none") - if (count === 1) return language.t("workspace.reset.archived.one") - return language.t("workspace.reset.archived.many", { count }) - } - - return ( - -
-
- - {language.t("workspace.reset.confirm", { name: name() })} - - - {description()} {archivedLabel()} {language.t("workspace.reset.note")} - -
-
- - -
-
-
- ) - } - - const activeRoute = { - session: "", - sessionProject: "", - directory: "", - } - - createEffect( - on( - () => { - return [pageReady(), route().slug, params.id, currentProject()?.worktree, currentDir()] as const - }, - ([ready, slug, id, root, dir]) => { - if (!ready || !slug || !dir) { - activeRoute.session = "" - activeRoute.sessionProject = "" - activeRoute.directory = "" - return - } - - if (!id) { - activeRoute.session = "" - activeRoute.sessionProject = "" - activeRoute.directory = "" - return - } - - const session = `${slug}/${id}` - - if (!root) { - activeRoute.session = session - activeRoute.directory = dir - activeRoute.sessionProject = "" - return - } - - if (server.projects.last() !== root) server.projects.touch(root) - - const changed = session !== activeRoute.session || dir !== activeRoute.directory - if (changed) { - activeRoute.session = session - activeRoute.directory = dir - activeRoute.sessionProject = syncSessionRoute(dir, id, root) - return - } - - if (root === activeRoute.sessionProject) return - activeRoute.directory = dir - activeRoute.sessionProject = rememberSessionRoute(dir, id, root) - }, - ), - ) - - createEffect(() => { - const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48 - document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`) - }) - - const side = createMemo(() => Math.max(layout.sidebar.width(), 244)) - const panel = createMemo(() => Math.max(side() - 64, 0)) - - const loadedSessionDirs = new Set() - - createEffect( - on( - visibleSessionDirs, - (dirs) => { - if (dirs.length === 0) { - loadedSessionDirs.clear() - return - } - - const next = new Set(dirs) - for (const directory of next) { - if (loadedSessionDirs.has(directory)) continue - void globalSync.project.loadSessions(directory) - } - - loadedSessionDirs.clear() - for (const directory of next) { - loadedSessionDirs.add(directory) - } - }, - { defer: true }, - ), - ) - - function handleDragStart(event: unknown) { - const id = getDraggableId(event) - if (!id) return - setHoverProject(undefined) - setStore("activeProject", id) - } - - function handleDragOver(event: DragEvent) { - const { draggable, droppable } = event - if (draggable && droppable) { - const projects = layout.projects.list() - const fromIndex = projects.findIndex((p) => p.worktree === draggable.id.toString()) - const toIndex = projects.findIndex((p) => p.worktree === droppable.id.toString()) - if (fromIndex !== toIndex && toIndex !== -1) { - layout.projects.move(draggable.id.toString(), toIndex) - } - } - } - - function handleDragEnd() { - setStore("activeProject", undefined) - } - - function workspaceIds(project: LocalProject | undefined) { - if (!project) return [] - const local = project.worktree - const dirs = [local, ...(project.sandboxes ?? [])] - const active = currentProject() - const directory = workspaceKey(active?.worktree ?? "") === workspaceKey(project.worktree) ? currentDir() : undefined - const extra = - directory && - workspaceKey(directory) !== workspaceKey(local) && - !dirs.some((item) => workspaceKey(item) === workspaceKey(directory)) - ? directory - : undefined - const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false - - const ordered = effectiveWorkspaceOrder(local, dirs, store.workspaceOrder[project.worktree]) - if (pending && extra) return [local, extra, ...ordered.filter((item) => item !== local)] - if (!extra) return ordered - if (pending) return ordered - return [...ordered, extra] - } - - const sidebarProject = createMemo(() => { - if (layout.sidebar.opened()) return currentProject() - const hovered = hoverProjectData() - if (hovered) return hovered - return currentProject() - }) - - function handleWorkspaceDragStart(event: unknown) { - const id = getDraggableId(event) - if (!id) return - setStore("activeWorkspace", id) - } - - function handleWorkspaceDragOver(event: DragEvent) { - const { draggable, droppable } = event - if (!draggable || !droppable) return - - const project = sidebarProject() - if (!project) return - - const ids = workspaceIds(project) - const fromIndex = ids.findIndex((dir) => dir === draggable.id.toString()) - const toIndex = ids.findIndex((dir) => dir === droppable.id.toString()) - if (fromIndex === -1 || toIndex === -1) return - if (fromIndex === toIndex) return - - const result = ids.slice() - const [item] = result.splice(fromIndex, 1) - if (!item) return - result.splice(toIndex, 0, item) - setStore( - "workspaceOrder", - project.worktree, - result.filter((directory) => workspaceKey(directory) !== workspaceKey(project.worktree)), - ) - } - - function handleWorkspaceDragEnd() { - setStore("activeWorkspace", undefined) - } - - const createWorkspace = async (project: LocalProject) => { - clearSidebarHoverState() - const created = await globalSDK.client.worktree - .create({ directory: project.worktree }) - .then((x) => x.data) - .catch((err) => { - showToast({ - title: language.t("workspace.create.failed.title"), - description: errorMessage(err, language.t("common.requestFailed")), - }) - return undefined - }) - - if (!created?.directory) return - - setWorkspaceName(created.directory, created.branch, project.id, created.branch) - - const local = project.worktree - const key = workspaceKey(created.directory) - const root = workspaceKey(local) - - setBusy(created.directory, true) - WorktreeState.pending(created.directory) - setStore("workspaceExpanded", key, true) - if (key !== created.directory) { - setStore("workspaceExpanded", created.directory, true) - } - setStore("workspaceOrder", project.worktree, (prev) => { - const existing = prev ?? [] - const next = existing.filter((item) => { - const id = workspaceKey(item) - return id !== root && id !== key - }) - return [created.directory, ...next] - }) - - globalSync.child(created.directory) - navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`) - } - - const workspaceSidebarCtx: WorkspaceSidebarContext = { - currentDir, - navList: currentSessions, - sidebarExpanded, - sidebarHovering, - clearHoverProjectSoon, - prefetchSession, - archiveSession, - workspaceName, - renameWorkspace, - editorOpen, - openEditor, - closeEditor, - setEditor, - InlineEditor, - isBusy, - workspaceExpanded: (directory, local) => store.workspaceExpanded[directory] ?? local, - setWorkspaceExpanded: (directory, value) => setStore("workspaceExpanded", directory, value), - showResetWorkspaceDialog: (root, directory) => - dialog.show(() => ), - showDeleteWorkspaceDialog: (root, directory) => - dialog.show(() => ), - setScrollContainerRef: (el, mobile) => { - if (!mobile) scrollContainerRef = el - }, - } - - const projectSidebarCtx: ProjectSidebarContext = { - currentDir, - currentProject, - sidebarOpened: () => layout.sidebar.opened(), - sidebarHovering, - hoverProject: () => state.hoverProject, - onProjectMouseEnter: (worktree, event) => aim.enter(worktree, event), - onProjectMouseLeave: (worktree) => aim.leave(worktree), - onProjectFocus: (worktree) => aim.activate(worktree), - onHoverOpenChanged: (worktree, hoverOpen) => { - if (!hoverOpen && state.hoverProject && state.hoverProject !== worktree) return - setState("hoverProject", hoverOpen ? worktree : undefined) - }, - navigateToProject, - openSidebar: () => layout.sidebar.open(), - closeProject, - showEditProjectDialog, - toggleProjectWorkspaces, - workspacesEnabled: (project) => project.vcs === "git" && layout.sidebar.workspaces(project.worktree)(), - workspaceIds, - workspaceLabel, - sessionProps: { - navList: currentSessions, - sidebarExpanded, - clearHoverProjectSoon, - prefetchSession, - archiveSession, - }, - } - - const SidebarPanel = (panelProps: { - project: Accessor - mobile?: boolean - merged?: boolean - }) => { - const project = panelProps.project - const merged = createMemo(() => panelProps.mobile || (panelProps.merged ?? layout.sidebar.opened())) - const hover = createMemo(() => !panelProps.mobile && panelProps.merged === false && !layout.sidebar.opened()) - const empty = createMemo(() => !params.dir && layout.projects.list().length === 0) - const projectName = createMemo(() => { - const item = project() - if (!item) return "" - return item.name || getFilename(item.worktree) - }) - const projectId = createMemo(() => project()?.id ?? "") - const worktree = createMemo(() => project()?.worktree ?? "") - const slug = createMemo(() => { - const dir = worktree() - if (!dir) return "" - return base64Encode(dir) - }) - const workspaces = createMemo(() => { - const item = project() - if (!item) return [] as string[] - return workspaceIds(item) - }) - const unseenCount = createMemo(() => - workspaces().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), - ) - const clearNotifications = () => - workspaces() - .filter((directory) => notification.project.unseenCount(directory) > 0) - .forEach((directory) => notification.project.markViewed(directory)) - const workspacesEnabled = createMemo(() => { - const item = project() - if (!item) return false - if (item.vcs !== "git") return false - return layout.sidebar.workspaces(item.worktree)() - }) - const canToggle = createMemo(() => { - const item = project() - if (!item) return false - return item.vcs === "git" || layout.sidebar.workspaces(item.worktree)() - }) - const homedir = createMemo(() => globalSync.data.path.home) - - return ( -
- -
-
-
-
{language.t("sidebar.empty.title")}
-
- {language.t("sidebar.empty.description")} -
-
- -
-
-
- } - > - {(project) => ( - <> -
-
-
- { - const item = project() - if (!item) return - void renameProject(item, next) - }} - class="text-14-medium text-text-strong truncate" - displayClass="text-14-medium text-text-strong truncate" - stopPropagation - /> - - - - {worktree().replace(homedir(), "~")} - - -
- - - - - - { - const item = project() - if (!item) return - showEditProjectDialog(item) - }} - > - {language.t("common.edit")} - - { - const item = project() - if (!item) return - toggleProjectWorkspaces(item) - }} - > - - {workspacesEnabled() - ? language.t("sidebar.workspaces.disable") - : language.t("sidebar.workspaces.enable")} - - - - - {language.t("sidebar.project.clearNotifications")} - - - - { - const dir = worktree() - if (!dir) return - closeProject(dir) - }} - > - {language.t("common.close")} - - - - -
-
- -
- -
- -
-
- -
- - } - > - <> -
- -
-
- - - -
{ - if (!panelProps.mobile) scrollContainerRef = el - }} - class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]" - > - - - {(directory) => ( - - )} - - -
- - store.activeWorkspace} - workspaceLabel={workspaceLabel} - /> - -
-
- -
-
- - )} - - -
0 && providers.paid().length === 0), - }} - > -
-
-
-
{language.t("sidebar.gettingStarted.title")}
-
- {language.t("sidebar.gettingStarted.line1")} -
-
- {language.t("sidebar.gettingStarted.line2")} -
-
-
- - -
-
-
-
-
- ) - } - - const projects = () => layout.projects.list() - const projectOverlay = () => store.activeProject} /> - const sidebarContent = (mobile?: boolean) => ( - layout.sidebar.opened()} - aimMove={aim.move} - projects={projects} - renderProject={(project) => ( - - )} - handleDragStart={handleDragStart} - handleDragEnd={handleDragEnd} - handleDragOver={handleDragOver} - openProjectLabel={language.t("command.project.open")} - openProjectKeybind={() => command.keybind("project.open")} - onOpenProject={chooseProject} - renderProjectOverlay={projectOverlay} - settingsLabel={() => language.t("sidebar.settings")} - settingsKeybind={() => command.keybind("settings.open")} - onOpenSettings={openSettings} - helpLabel={() => language.t("sidebar.help")} - onOpenHelp={() => platform.openLink("https://kilo.ai/desktop-feedback")} - renderPanel={() => - mobile ? : - } - /> - ) - - return ( -
- {autoselecting() ?? ""} - -
-
-
- - - - - - - - -
- ) -} diff --git a/packages/app/src/pages/layout/deep-links.ts b/packages/app/src/pages/layout/deep-links.ts deleted file mode 100644 index 67137f17fa9..00000000000 --- a/packages/app/src/pages/layout/deep-links.ts +++ /dev/null @@ -1,50 +0,0 @@ -export const deepLinkEvent = "opencode:deep-link" - -const parseUrl = (input: string) => { - if (!input.startsWith("opencode://")) return - if (typeof URL.canParse === "function" && !URL.canParse(input)) return - try { - return new URL(input) - } catch { - return - } -} - -export const parseDeepLink = (input: string) => { - const url = parseUrl(input) - if (!url) return - if (url.hostname !== "open-project") return - const directory = url.searchParams.get("directory") - if (!directory) return - return directory -} - -export const parseNewSessionDeepLink = (input: string) => { - const url = parseUrl(input) - if (!url) return - if (url.hostname !== "new-session") return - const directory = url.searchParams.get("directory") - if (!directory) return - const prompt = url.searchParams.get("prompt") || undefined - if (!prompt) return { directory } - return { directory, prompt } -} - -export const collectOpenProjectDeepLinks = (urls: string[]) => - urls.map(parseDeepLink).filter((directory): directory is string => !!directory) - -export const collectNewSessionDeepLinks = (urls: string[]) => - urls.map(parseNewSessionDeepLink).filter((link): link is { directory: string; prompt?: string } => !!link) - -type OpenCodeWindow = Window & { - __KILO__?: { - deepLinks?: string[] - } -} - -export const drainPendingDeepLinks = (target: OpenCodeWindow) => { - const pending = target.__KILO__?.deepLinks ?? [] - if (pending.length === 0) return [] - if (target.__KILO__) target.__KILO__.deepLinks = [] - return pending -} diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts deleted file mode 100644 index e33afdfc415..00000000000 --- a/packages/app/src/pages/layout/helpers.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { - collectNewSessionDeepLinks, - collectOpenProjectDeepLinks, - drainPendingDeepLinks, - parseDeepLink, - parseNewSessionDeepLink, -} from "./deep-links" -import { type Session } from "@kilocode/sdk/v2/client" -import { - childSessionOnPath, - displayName, - effectiveWorkspaceOrder, - errorMessage, - hasProjectPermissions, - latestRootSession, - workspaceKey, -} from "./helpers" - -const session = (input: Partial & Pick) => - ({ - title: "", - version: "v2", - parentID: undefined, - messageCount: 0, - permissions: { session: {}, share: {} }, - time: { created: 0, updated: 0, archived: undefined }, - ...input, - }) as Session - -describe("layout deep links", () => { - test("parses open-project deep links", () => { - expect(parseDeepLink("opencode://open-project?directory=/tmp/demo")).toBe("/tmp/demo") - }) - - test("ignores non-project deep links", () => { - expect(parseDeepLink("opencode://other?directory=/tmp/demo")).toBeUndefined() - expect(parseDeepLink("https://example.com")).toBeUndefined() - }) - - test("ignores malformed deep links safely", () => { - expect(() => parseDeepLink("opencode://open-project/%E0%A4%A%")).not.toThrow() - expect(parseDeepLink("opencode://open-project/%E0%A4%A%")).toBeUndefined() - }) - - test("parses links when URL.canParse is unavailable", () => { - const original = Object.getOwnPropertyDescriptor(URL, "canParse") - Object.defineProperty(URL, "canParse", { configurable: true, value: undefined }) - try { - expect(parseDeepLink("opencode://open-project?directory=/tmp/demo")).toBe("/tmp/demo") - } finally { - if (original) Object.defineProperty(URL, "canParse", original) - if (!original) Reflect.deleteProperty(URL, "canParse") - } - }) - - test("ignores open-project deep links without directory", () => { - expect(parseDeepLink("opencode://open-project")).toBeUndefined() - expect(parseDeepLink("opencode://open-project?directory=")).toBeUndefined() - }) - - test("collects only valid open-project directories", () => { - const result = collectOpenProjectDeepLinks([ - "opencode://open-project?directory=/a", - "opencode://other?directory=/b", - "opencode://open-project?directory=/c", - ]) - expect(result).toEqual(["/a", "/c"]) - }) - - test("parses new-session deep links with optional prompt", () => { - expect(parseNewSessionDeepLink("opencode://new-session?directory=/tmp/demo")).toEqual({ directory: "/tmp/demo" }) - expect(parseNewSessionDeepLink("opencode://new-session?directory=/tmp/demo&prompt=hello%20world")).toEqual({ - directory: "/tmp/demo", - prompt: "hello world", - }) - }) - - test("ignores new-session deep links without directory", () => { - expect(parseNewSessionDeepLink("opencode://new-session")).toBeUndefined() - expect(parseNewSessionDeepLink("opencode://new-session?directory=")).toBeUndefined() - }) - - test("collects only valid new-session deep links", () => { - const result = collectNewSessionDeepLinks([ - "opencode://new-session?directory=/a", - "opencode://open-project?directory=/b", - "opencode://new-session?directory=/c&prompt=ship%20it", - ]) - expect(result).toEqual([{ directory: "/a" }, { directory: "/c", prompt: "ship it" }]) - }) - - test("drains global deep links once", () => { - const target = { - __KILO__: { - deepLinks: ["opencode://open-project?directory=/a"], - }, - } as unknown as Window & { __KILO__?: { deepLinks?: string[] } } - - expect(drainPendingDeepLinks(target)).toEqual(["opencode://open-project?directory=/a"]) - expect(drainPendingDeepLinks(target)).toEqual([]) - }) -}) - -describe("layout workspace helpers", () => { - test("normalizes trailing slash in workspace key", () => { - expect(workspaceKey("/tmp/demo///")).toBe("/tmp/demo") - expect(workspaceKey("C:\\tmp\\demo\\\\")).toBe("C:/tmp/demo") - }) - - test("preserves posix and drive roots in workspace key", () => { - expect(workspaceKey("/")).toBe("/") - expect(workspaceKey("///")).toBe("/") - expect(workspaceKey("C:\\")).toBe("C:/") - expect(workspaceKey("C://")).toBe("C:/") - expect(workspaceKey("C:///")).toBe("C:/") - }) - - test("keeps local first while preserving known order", () => { - const result = effectiveWorkspaceOrder("/root", ["/root", "/b", "/c"], ["/root", "/c", "/a", "/b"]) - expect(result).toEqual(["/root", "/c", "/b"]) - }) - - test("finds the latest root session across workspaces", () => { - const result = latestRootSession( - [ - { - path: { directory: "/root" }, - session: [session({ id: "root", directory: "/root", time: { created: 1, updated: 1, archived: undefined } })], - }, - { - path: { directory: "/workspace" }, - session: [ - session({ - id: "workspace", - directory: "/workspace", - time: { created: 2, updated: 2, archived: undefined }, - }), - ], - }, - ], - 120_000, - ) - - expect(result?.id).toBe("workspace") - }) - - test("detects project permissions with a filter", () => { - const result = hasProjectPermissions( - { - root: [{ id: "perm-root" }, { id: "perm-hidden" }], - child: [{ id: "perm-child" }], - }, - (item) => item.id === "perm-child", - ) - - expect(result).toBe(true) - }) - - test("ignores project permissions filtered out", () => { - const result = hasProjectPermissions( - { - root: [{ id: "perm-root" }], - }, - () => false, - ) - - expect(result).toBe(false) - }) - - test("ignores archived and child sessions when finding latest root session", () => { - const result = latestRootSession( - [ - { - path: { directory: "/workspace" }, - session: [ - session({ - id: "archived", - directory: "/workspace", - time: { created: 10, updated: 10, archived: 10 }, - }), - session({ - id: "child", - directory: "/workspace", - parentID: "parent", - time: { created: 20, updated: 20, archived: undefined }, - }), - session({ - id: "root", - directory: "/workspace", - time: { created: 30, updated: 30, archived: undefined }, - }), - ], - }, - ], - 120_000, - ) - - expect(result?.id).toBe("root") - }) - - test("finds the direct child on the active session path", () => { - const list = [ - session({ id: "root", directory: "/workspace" }), - session({ id: "child", directory: "/workspace", parentID: "root" }), - session({ id: "leaf", directory: "/workspace", parentID: "child" }), - ] - - expect(childSessionOnPath(list, "root", "leaf")?.id).toBe("child") - expect(childSessionOnPath(list, "child", "leaf")?.id).toBe("leaf") - expect(childSessionOnPath(list, "root", "root")).toBeUndefined() - expect(childSessionOnPath(list, "root", "other")).toBeUndefined() - }) - - test("formats fallback project display name", () => { - expect(displayName({ worktree: "/tmp/app" })).toBe("app") - expect(displayName({ worktree: "/tmp/app", name: "My App" })).toBe("My App") - }) - - test("extracts api error message and fallback", () => { - expect(errorMessage({ data: { message: "boom" } }, "fallback")).toBe("boom") - expect(errorMessage(new Error("broken"), "fallback")).toBe("broken") - expect(errorMessage("unknown", "fallback")).toBe("fallback") - }) -}) diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts deleted file mode 100644 index d1723b6e27a..00000000000 --- a/packages/app/src/pages/layout/helpers.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { getFilename } from "@opencode-ai/shared/util/path" -import { type Session } from "@kilocode/sdk/v2/client" - -type SessionStore = { - session?: Session[] - path: { directory: string } -} - -export const workspaceKey = (directory: string) => { - const value = directory.replaceAll("\\", "/") - const drive = value.match(/^([A-Za-z]:)\/+$/) - if (drive) return `${drive[1]}/` - if (/^\/+$/i.test(value)) return "/" - return value.replace(/\/+$/, "") -} - -function sortSessions(now: number) { - const oneMinuteAgo = now - 60 * 1000 - return (a: Session, b: Session) => { - const aUpdated = a.time.updated ?? a.time.created - const bUpdated = b.time.updated ?? b.time.created - const aRecent = aUpdated > oneMinuteAgo - const bRecent = bUpdated > oneMinuteAgo - if (aRecent && bRecent) return a.id < b.id ? -1 : a.id > b.id ? 1 : 0 - if (aRecent && !bRecent) return -1 - if (!aRecent && bRecent) return 1 - return bUpdated - aUpdated - } -} - -const isRootVisibleSession = (session: Session, directory: string) => - workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived - -export const roots = (store: SessionStore) => - (store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory)) - -export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now)) - -export const latestRootSession = (stores: SessionStore[], now: number) => - stores.flatMap(roots).sort(sortSessions(now))[0] - -export function hasProjectPermissions( - request: Record | undefined, - include: (item: T) => boolean = () => true, -) { - return Object.values(request ?? {}).some((list) => list?.some(include)) -} - -export const childSessionOnPath = (sessions: Session[] | undefined, rootID: string, activeID?: string) => { - if (!activeID || activeID === rootID) return - const map = new Map((sessions ?? []).map((session) => [session.id, session])) - let id = activeID - - while (id) { - const session = map.get(id) - if (!session?.parentID) return - if (session.parentID === rootID) return session - id = session.parentID - } -} - -export const displayName = (project: { name?: string; worktree: string }) => - project.name || getFilename(project.worktree) - -export const errorMessage = (err: unknown, fallback: string) => { - if (err && typeof err === "object" && "data" in err) { - const data = (err as { data?: { message?: string } }).data - if (data?.message) return data.message - } - if (err instanceof Error) return err.message - return fallback -} - -export const effectiveWorkspaceOrder = (local: string, dirs: string[], persisted?: string[]) => { - const root = workspaceKey(local) - const live = new Map() - - for (const dir of dirs) { - const key = workspaceKey(dir) - if (key === root) continue - if (!live.has(key)) live.set(key, dir) - } - - if (!persisted?.length) return [local, ...live.values()] - - const result = [local] - for (const dir of persisted) { - const key = workspaceKey(dir) - if (key === root) continue - const match = live.get(key) - if (!match) continue - result.push(match) - live.delete(key) - } - - return [...result, ...live.values()] -} diff --git a/packages/app/src/pages/layout/inline-editor.tsx b/packages/app/src/pages/layout/inline-editor.tsx deleted file mode 100644 index 4189e4a72a0..00000000000 --- a/packages/app/src/pages/layout/inline-editor.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { createStore } from "solid-js/store" -import { onCleanup, Show, type Accessor } from "solid-js" -import { InlineInput } from "@opencode-ai/ui/inline-input" - -export function createInlineEditorController() { - // This controller intentionally supports one active inline editor at a time. - const [editor, setEditor] = createStore({ - active: "" as string, - value: "", - }) - - const editorOpen = (id: string) => editor.active === id - const editorValue = () => editor.value - const openEditor = (id: string, value: string) => { - if (!id) return - setEditor({ active: id, value }) - } - const closeEditor = () => setEditor({ active: "", value: "" }) - - const saveEditor = (callback: (next: string) => void) => { - const next = editor.value.trim() - if (!next) { - closeEditor() - return - } - closeEditor() - callback(next) - } - - const editorKeyDown = (event: KeyboardEvent, callback: (next: string) => void) => { - if (event.key === "Enter") { - event.preventDefault() - saveEditor(callback) - return - } - if (event.key !== "Escape") return - event.preventDefault() - closeEditor() - } - - const InlineEditor = (props: { - id: string - value: Accessor - onSave: (next: string) => void - class?: string - displayClass?: string - editing?: boolean - stopPropagation?: boolean - openOnDblClick?: boolean - }) => { - let frame: number | undefined - - onCleanup(() => { - if (frame === undefined) return - cancelAnimationFrame(frame) - }) - - const isEditing = () => props.editing ?? editorOpen(props.id) - const stopEvents = () => props.stopPropagation ?? false - const allowDblClick = () => props.openOnDblClick ?? true - const stopPropagation = (event: Event) => { - if (!stopEvents()) return - event.stopPropagation() - } - const handleDblClick = (event: MouseEvent) => { - if (!allowDblClick()) return - stopPropagation(event) - openEditor(props.id, props.value()) - } - - return ( - - {props.value()} - - } - > - { - if (frame !== undefined) cancelAnimationFrame(frame) - frame = requestAnimationFrame(() => { - frame = undefined - if (!el.isConnected) return - el.focus() - }) - }} - value={editorValue()} - class={props.class} - onInput={(event) => setEditor("value", event.currentTarget.value)} - onKeyDown={(event) => { - event.stopPropagation() - editorKeyDown(event, props.onSave) - }} - onBlur={closeEditor} - onPointerDown={stopPropagation} - onClick={stopPropagation} - onDblClick={stopPropagation} - onMouseDown={stopPropagation} - onMouseUp={stopPropagation} - onTouchStart={stopPropagation} - /> - - ) - } - - return { - editor, - editorOpen, - editorValue, - openEditor, - closeEditor, - saveEditor, - editorKeyDown, - setEditor, - InlineEditor, - } -} diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx deleted file mode 100644 index adb46170a87..00000000000 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ /dev/null @@ -1,334 +0,0 @@ -import type { Session } from "@kilocode/sdk/v2/client" -import { Avatar } from "@opencode-ai/ui/avatar" -import { Icon } from "@opencode-ai/ui/icon" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Spinner } from "@opencode-ai/ui/spinner" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { getFilename } from "@opencode-ai/shared/util/path" -import { A, useParams } from "@solidjs/router" -import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js" -import { useGlobalSync } from "@/context/global-sync" -import { useLanguage } from "@/context/language" -import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout" -import { useNotification } from "@/context/notification" -import { usePermission } from "@/context/permission" -import { messageAgentColor } from "@/utils/agent" -import { sessionTitle } from "@/utils/session-title" -import { sessionPermissionRequest } from "../session/composer/session-request-tree" -import { childSessionOnPath, hasProjectPermissions } from "./helpers" - -const KILO_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" - -export function getProjectAvatarSource(id?: string, icon?: { color?: string; url?: string; override?: string }) { - return id === KILO_PROJECT_ID - ? "https://kilo.ai/favicon.svg" - : (icon?.override ?? (icon?.color ? undefined : icon?.url)) -} - -export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => { - const globalSync = useGlobalSync() - const notification = useNotification() - const permission = usePermission() - const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])]) - const unseenCount = createMemo(() => - dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), - ) - const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory))) - const hasPermissions = createMemo(() => - dirs().some((directory) => { - const [store] = globalSync.child(directory, { bootstrap: false }) - return hasProjectPermissions(store.permission, (item) => !permission.autoResponds(item, directory)) - }), - ) - const notify = createMemo(() => props.notify && (hasPermissions() || unseenCount() > 0)) - const name = createMemo(() => props.project.name || getFilename(props.project.worktree)) - - return ( -
-
- -
- -
- -
- ) -} - -export type SessionItemProps = { - session: Session - list: Session[] - navList?: Accessor - slug: string - mobile?: boolean - dense?: boolean - showTooltip?: boolean - showChild?: boolean - level?: number - sidebarExpanded: Accessor - clearHoverProjectSoon: () => void - prefetchSession: (session: Session, priority?: "high" | "low") => void - archiveSession: (session: Session) => Promise -} - -const SessionRow = (props: { - session: Session - slug: string - mobile?: boolean - dense?: boolean - tint: Accessor - isWorking: Accessor - hasPermissions: Accessor - hasError: Accessor - unseenCount: Accessor - clearHoverProjectSoon: () => void - sidebarOpened: Accessor - warmPress: () => void - warmFocus: () => void -}): JSX.Element => { - const title = () => sessionTitle(props.session.title) - - return ( - { - if (props.sidebarOpened()) return - props.clearHoverProjectSoon() - }} - > - 0}> -
- - - - - -
- - -
- - 0}> -
- - -
- - {title()} -
- ) -} - -export const SessionItem = (props: SessionItemProps): JSX.Element => { - const params = useParams() - const layout = useLayout() - const language = useLanguage() - const notification = useNotification() - const permission = usePermission() - const globalSync = useGlobalSync() - const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id)) - const hasError = createMemo(() => notification.session.unseenHasError(props.session.id)) - const [sessionStore] = globalSync.child(props.session.directory) - const hasPermissions = createMemo(() => { - return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => { - return !permission.autoResponds(item, props.session.directory) - }) - }) - const isWorking = createMemo(() => { - if (hasPermissions()) return false - const pending = (sessionStore.message[props.session.id] ?? []).findLast( - (message) => - message.role === "assistant" && - typeof (message as { time?: { completed?: unknown } }).time?.completed !== "number", - ) - const status = sessionStore.session_status[props.session.id] - return ( - pending !== undefined || - status?.type === "busy" || - status?.type === "retry" || - (status !== undefined && status.type !== "idle") - ) - }) - - const tint = createMemo(() => messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent)) - const tooltip = createMemo(() => props.showTooltip ?? (props.mobile || !props.sidebarExpanded())) - const currentChild = createMemo(() => { - if (!props.showChild) return - return childSessionOnPath(sessionStore.session, props.session.id, params.id) - }) - - const warm = (span: number, priority: "high" | "low") => { - const nav = props.navList?.() - const list = nav?.some((item) => item.id === props.session.id && item.directory === props.session.directory) - ? nav - : props.list - - props.prefetchSession(props.session, priority) - - const idx = list.findIndex((item) => item.id === props.session.id && item.directory === props.session.directory) - if (idx === -1) return - - for (let step = 1; step <= span; step++) { - const next = list[idx + step] - if (next) props.prefetchSession(next, step === 1 ? "high" : priority) - - const prev = list[idx - step] - if (prev) props.prefetchSession(prev, step === 1 ? "high" : priority) - } - } - - const item = ( - warm(2, "high")} - warmFocus={() => warm(2, "high")} - /> - ) - - return ( - <> -
-
-
- - {item} - - } - > - {item} - -
- - -
- - { - event.preventDefault() - event.stopPropagation() - void props.archiveSession(props.session) - }} - /> - -
-
-
-
- - {(child) => ( -
- -
- )} -
- - ) -} - -export const NewSessionItem = (props: { - slug: string - mobile?: boolean - dense?: boolean - sidebarExpanded: Accessor - clearHoverProjectSoon: () => void -}): JSX.Element => { - const layout = useLayout() - const language = useLanguage() - const label = language.t("command.session.new") - const tooltip = () => props.mobile || !props.sidebarExpanded() - const item = ( - { - if (layout.sidebar.opened()) return - props.clearHoverProjectSoon() - }} - > -
- -
- {label} -
- ) - - return ( -
- - {item} - - } - > - {item} - -
- ) -} - -export const SessionSkeleton = (props: { count?: number }): JSX.Element => { - const items = Array.from({ length: props.count ?? 4 }, (_, index) => index) - return ( -
- - {() =>
} - -
- ) -} diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx deleted file mode 100644 index 076e1ef88b5..00000000000 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ /dev/null @@ -1,367 +0,0 @@ -import { createMemo, For, Show, type Accessor, type JSX } from "solid-js" -import { createStore } from "solid-js/store" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { Button } from "@opencode-ai/ui/button" -import { ContextMenu } from "@opencode-ai/ui/context-menu" -import { HoverCard } from "@opencode-ai/ui/hover-card" -import { Icon } from "@opencode-ai/ui/icon" -import { createSortable } from "@thisbeyond/solid-dnd" -import { useLayout, type LocalProject } from "@/context/layout" -import { useGlobalSync } from "@/context/global-sync" -import { useLanguage } from "@/context/language" -import { useNotification } from "@/context/notification" -import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items" -import { displayName, sortedRootSessions } from "./helpers" - -export type ProjectSidebarContext = { - currentDir: Accessor - currentProject: Accessor - sidebarOpened: Accessor - sidebarHovering: Accessor - hoverProject: Accessor - onProjectMouseEnter: (worktree: string, event: MouseEvent) => void - onProjectMouseLeave: (worktree: string) => void - onProjectFocus: (worktree: string) => void - onHoverOpenChanged: (worktree: string, hovered: boolean) => void - navigateToProject: (directory: string) => void - openSidebar: () => void - closeProject: (directory: string) => void - showEditProjectDialog: (project: LocalProject) => void - toggleProjectWorkspaces: (project: LocalProject) => void - workspacesEnabled: (project: LocalProject) => boolean - workspaceIds: (project: LocalProject) => string[] - workspaceLabel: (directory: string, branch?: string, projectId?: string) => string - sessionProps: Omit -} - -export const ProjectDragOverlay = (props: { - projects: Accessor - activeProject: Accessor -}): JSX.Element => { - const project = createMemo(() => props.projects().find((p) => p.worktree === props.activeProject())) - return ( - - {(p) => ( -
- -
- )} -
- ) -} - -const ProjectTile = (props: { - project: LocalProject - mobile?: boolean - sidebarHovering: Accessor - selected: Accessor - active: Accessor - overlay: Accessor - suppressHover: Accessor - dirs: Accessor - onProjectMouseEnter: (worktree: string, event: MouseEvent) => void - onProjectMouseLeave: (worktree: string) => void - onProjectFocus: (worktree: string) => void - navigateToProject: (directory: string) => void - showEditProjectDialog: (project: LocalProject) => void - toggleProjectWorkspaces: (project: LocalProject) => void - workspacesEnabled: (project: LocalProject) => boolean - closeProject: (directory: string) => void - setMenu: (value: boolean) => void - setOpen: (value: boolean) => void - setSuppressHover: (value: boolean) => void - language: ReturnType -}): JSX.Element => { - const notification = useNotification() - const layout = useLayout() - const unseenCount = createMemo(() => - props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), - ) - - const clear = () => - props - .dirs() - .filter((directory) => notification.project.unseenCount(directory) > 0) - .forEach((directory) => notification.project.markViewed(directory)) - - return ( - { - props.setMenu(value) - props.setSuppressHover(value) - if (value) props.setOpen(false) - }} - > - { - if (event.button === 0 && !event.ctrlKey) { - props.setOpen(false) - props.setSuppressHover(true) - return - } - if (!props.overlay()) return - if (event.button !== 2 && !(event.button === 0 && event.ctrlKey)) return - props.setOpen(false) - props.setSuppressHover(true) - event.preventDefault() - }} - onMouseEnter={(event: MouseEvent) => { - if (!props.overlay()) return - if (props.suppressHover()) return - props.onProjectMouseEnter(props.project.worktree, event) - }} - onMouseLeave={() => { - if (props.suppressHover()) props.setSuppressHover(false) - if (!props.overlay()) return - props.onProjectMouseLeave(props.project.worktree) - }} - onFocus={() => { - if (!props.overlay()) return - if (props.suppressHover()) return - props.onProjectFocus(props.project.worktree) - }} - onClick={() => { - props.setOpen(false) - if (props.selected()) { - layout.sidebar.toggle() - return - } - props.navigateToProject(props.project.worktree) - }} - onBlur={() => props.setOpen(false)} - > - - - - - props.showEditProjectDialog(props.project)}> - {props.language.t("common.edit")} - - props.toggleProjectWorkspaces(props.project)} - > - - {props.workspacesEnabled(props.project) - ? props.language.t("sidebar.workspaces.disable") - : props.language.t("sidebar.workspaces.enable")} - - - - {props.language.t("sidebar.project.clearNotifications")} - - - props.closeProject(props.project.worktree)} - > - {props.language.t("common.close")} - - - - - ) -} - -const ProjectPreviewPanel = (props: { - project: LocalProject - mobile?: boolean - selected: Accessor - workspaceEnabled: Accessor - workspaces: Accessor - label: (directory: string) => string - projectSessions: Accessor> - workspaceSessions: (directory: string) => ReturnType - ctx: ProjectSidebarContext - language: ReturnType -}): JSX.Element => ( -
-
-
{displayName(props.project)}
-
-
{props.language.t("sidebar.project.recentSessions")}
-
- - {(session) => ( - - )} - - } - > - - {(directory) => { - const sessions = createMemo(() => props.workspaceSessions(directory)) - return ( -
-
-
- -
- {props.label(directory)} -
- - {(session) => ( - - )} - -
- ) - }} -
-
-
-
- -
-
-) - -export const SortableProject = (props: { - project: LocalProject - mobile?: boolean - ctx: ProjectSidebarContext - sortNow: Accessor -}): JSX.Element => { - const globalSync = useGlobalSync() - const language = useLanguage() - const sortable = createSortable(props.project.worktree) - const selected = createMemo(() => props.ctx.currentProject()?.worktree === props.project.worktree) - const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2)) - const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project)) - const dirs = createMemo(() => props.ctx.workspaceIds(props.project)) - const [state, setState] = createStore({ - menu: false, - suppressHover: false, - }) - - const isHoverProject = () => props.ctx.hoverProject() === props.project.worktree - const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened()) - const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened()) - const active = createMemo(() => state.menu || (preview() ? isHoverProject() : overlay() && isHoverProject())) - - const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu - - const label = (directory: string) => { - const [data] = globalSync.child(directory, { bootstrap: false }) - const kind = - directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") - const name = props.ctx.workspaceLabel(directory, data.vcs?.branch, props.project.id) - return `${kind} : ${name}` - } - - const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0]) - const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow())) - const workspaceSessions = (directory: string) => { - const [data] = globalSync.child(directory, { bootstrap: false }) - return sortedRootSessions(data, props.sortNow()) - } - const tile = () => ( - state.suppressHover} - dirs={dirs} - onProjectMouseEnter={props.ctx.onProjectMouseEnter} - onProjectMouseLeave={props.ctx.onProjectMouseLeave} - onProjectFocus={props.ctx.onProjectFocus} - navigateToProject={props.ctx.navigateToProject} - showEditProjectDialog={props.ctx.showEditProjectDialog} - toggleProjectWorkspaces={props.ctx.toggleProjectWorkspaces} - workspacesEnabled={props.ctx.workspacesEnabled} - closeProject={props.ctx.closeProject} - setMenu={(value) => setState("menu", value)} - setOpen={(value) => props.ctx.onHoverOpenChanged(props.project.worktree, value)} - setSuppressHover={(value) => setState("suppressHover", value)} - language={language} - /> - ) - - return ( - // @ts-ignore -
- - { - if (state.menu) return - if (value && state.suppressHover) return - props.ctx.onHoverOpenChanged(props.project.worktree, value) - }} - > - - - -
- ) -} diff --git a/packages/app/src/pages/layout/sidebar-shell.tsx b/packages/app/src/pages/layout/sidebar-shell.tsx deleted file mode 100644 index ca36af2a421..00000000000 --- a/packages/app/src/pages/layout/sidebar-shell.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js" -import { - DragDropProvider, - DragDropSensors, - DragOverlay, - SortableProvider, - closestCenter, - type DragEvent, -} from "@thisbeyond/solid-dnd" -import { ConstrainDragXAxis } from "@/utils/solid-dnd" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { type LocalProject } from "@/context/layout" - -export const SidebarContent = (props: { - mobile?: boolean - opened: Accessor - aimMove: (event: MouseEvent) => void - projects: Accessor - renderProject: (project: LocalProject) => JSX.Element - handleDragStart: (event: unknown) => void - handleDragEnd: () => void - handleDragOver: (event: DragEvent) => void - openProjectLabel: JSX.Element - openProjectKeybind: Accessor - onOpenProject: () => void - renderProjectOverlay: () => JSX.Element - settingsLabel: Accessor - settingsKeybind: Accessor - onOpenSettings: () => void - helpLabel: Accessor - onOpenHelp: () => void - renderPanel: () => JSX.Element -}): JSX.Element => { - const expanded = createMemo(() => !!props.mobile || props.opened()) - const placement = () => (props.mobile ? "bottom" : "right") - let panel: HTMLDivElement | undefined - - createEffect(() => { - const el = panel - if (!el) return - if (expanded()) { - el.removeAttribute("inert") - return - } - el.setAttribute("inert", "") - }) - - return ( -
-
-
- - - -
- p.worktree)}> - {(project) => props.renderProject(project)} - - - {props.openProjectLabel} - - {props.openProjectKeybind()} - -
- } - > - - -
- {props.renderProjectOverlay()} - -
-
- - - - - - -
-
- -
{ - panel = el - }} - classList={{ "flex-1 flex h-full min-h-0 min-w-0 overflow-hidden": true, "pointer-events-none": !expanded() }} - aria-hidden={!expanded()} - > - {props.renderPanel()} -
-
- ) -} diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx deleted file mode 100644 index a39e28cdb3b..00000000000 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ /dev/null @@ -1,482 +0,0 @@ -import { useNavigate, useParams } from "@solidjs/router" -import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js" -import { createStore } from "solid-js/store" -import { createSortable } from "@thisbeyond/solid-dnd" -import { createMediaQuery } from "@solid-primitives/media" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { getFilename } from "@opencode-ai/shared/util/path" -import { Button } from "@opencode-ai/ui/button" -import { Collapsible } from "@opencode-ai/ui/collapsible" -import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" -import { Icon } from "@opencode-ai/ui/icon" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Spinner } from "@opencode-ai/ui/spinner" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { type Session } from "@kilocode/sdk/v2/client" -import { type LocalProject } from "@/context/layout" -import { loadSessionsQuery, useGlobalSync } from "@/context/global-sync" -import { useLanguage } from "@/context/language" -import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items" -import { sortedRootSessions, workspaceKey } from "./helpers" -import { useQuery } from "@tanstack/solid-query" - -type InlineEditorComponent = (props: { - id: string - value: Accessor - onSave: (next: string) => void - class?: string - displayClass?: string - editing?: boolean - stopPropagation?: boolean - openOnDblClick?: boolean -}) => JSX.Element - -export type WorkspaceSidebarContext = { - currentDir: Accessor - navList: Accessor - sidebarExpanded: Accessor - sidebarHovering: Accessor - clearHoverProjectSoon: () => void - prefetchSession: (session: Session, priority?: "high" | "low") => void - archiveSession: (session: Session) => Promise - workspaceName: (directory: string, projectId?: string, branch?: string) => string | undefined - renameWorkspace: (directory: string, next: string, projectId?: string, branch?: string) => void - editorOpen: (id: string) => boolean - openEditor: (id: string, value: string) => void - closeEditor: () => void - setEditor: (key: "value", value: string) => void - InlineEditor: InlineEditorComponent - isBusy: (directory: string) => boolean - workspaceExpanded: (directory: string, local: boolean) => boolean - setWorkspaceExpanded: (directory: string, value: boolean) => void - showResetWorkspaceDialog: (root: string, directory: string) => void - showDeleteWorkspaceDialog: (root: string, directory: string) => void - setScrollContainerRef: (el: HTMLDivElement | undefined, mobile?: boolean) => void -} - -export const WorkspaceDragOverlay = (props: { - sidebarProject: Accessor - activeWorkspace: Accessor - workspaceLabel: (directory: string, branch?: string, projectId?: string) => string -}): JSX.Element => { - const globalSync = useGlobalSync() - const language = useLanguage() - const label = createMemo(() => { - const project = props.sidebarProject() - if (!project) return - const directory = props.activeWorkspace() - if (!directory) return - - const [workspaceStore] = globalSync.child(directory, { bootstrap: false }) - const kind = - directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") - const name = props.workspaceLabel(directory, workspaceStore.vcs?.branch, project.id) - return `${kind} : ${name}` - }) - - return ( - - {(value) =>
{value()}
} -
- ) -} - -const WorkspaceHeader = (props: { - local: Accessor - busy: Accessor - open: Accessor - directory: string - language: ReturnType - branch: Accessor - workspaceValue: Accessor - workspaceEditActive: Accessor - InlineEditor: WorkspaceSidebarContext["InlineEditor"] - renameWorkspace: WorkspaceSidebarContext["renameWorkspace"] - setEditor: WorkspaceSidebarContext["setEditor"] - projectId?: string -}): JSX.Element => ( -
-
- }> - - -
- - {props.local() ? props.language.t("workspace.type.local") : props.language.t("workspace.type.sandbox")} : - - - {props.branch() ?? getFilename(props.directory)} - - } - > - { - const trimmed = next.trim() - if (!trimmed) return - props.renameWorkspace(props.directory, trimmed, props.projectId, props.branch()) - props.setEditor("value", props.workspaceValue()) - }} - class="text-14-medium text-text-base min-w-0 truncate" - displayClass="text-14-medium text-text-base min-w-0 truncate" - editing={props.workspaceEditActive()} - stopPropagation={false} - openOnDblClick={false} - /> - -
- -
-
-) - -const WorkspaceActions = (props: { - directory: string - local: Accessor - busy: Accessor - menuOpen: Accessor - pendingRename: Accessor - setMenuOpen: (open: boolean) => void - setPendingRename: (value: boolean) => void - sidebarHovering: Accessor - touch: Accessor - language: ReturnType - workspaceValue: Accessor - openEditor: WorkspaceSidebarContext["openEditor"] - showResetWorkspaceDialog: WorkspaceSidebarContext["showResetWorkspaceDialog"] - showDeleteWorkspaceDialog: WorkspaceSidebarContext["showDeleteWorkspaceDialog"] - root: string - clearHoverProjectSoon: WorkspaceSidebarContext["clearHoverProjectSoon"] - navigateToNewSession: () => void -}): JSX.Element => ( -
- props.setMenuOpen(open)} - > - - - - - { - if (!props.pendingRename()) return - event.preventDefault() - props.setPendingRename(false) - props.openEditor(`workspace:${props.directory}`, props.workspaceValue()) - }} - > - { - props.setPendingRename(true) - props.setMenuOpen(false) - }} - > - {props.language.t("common.rename")} - - props.showResetWorkspaceDialog(props.root, props.directory)} - > - {props.language.t("common.reset")} - - props.showDeleteWorkspaceDialog(props.root, props.directory)} - > - {props.language.t("common.delete")} - - - - - - - { - event.preventDefault() - event.stopPropagation() - props.clearHoverProjectSoon() - props.navigateToNewSession() - }} - /> - - -
-) - -const WorkspaceSessionList = (props: { - slug: Accessor - mobile?: boolean - ctx: WorkspaceSidebarContext - showNew: Accessor - loading: Accessor - sessions: Accessor - hasMore: Accessor - loadMore: () => Promise - language: ReturnType -}): JSX.Element => ( - -) - -export const SortableWorkspace = (props: { - ctx: WorkspaceSidebarContext - directory: string - project: LocalProject - sortNow: Accessor - mobile?: boolean -}): JSX.Element => { - const navigate = useNavigate() - const params = useParams() - const globalSync = useGlobalSync() - const language = useLanguage() - const sortable = createSortable(props.directory) - const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false }) - const [menu, setMenu] = createStore({ - open: false, - pendingRename: false, - }) - const slug = createMemo(() => base64Encode(props.directory)) - const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow())) - const local = createMemo(() => props.directory === props.project.worktree) - const active = createMemo(() => workspaceKey(props.ctx.currentDir()) === workspaceKey(props.directory)) - const workspaceValue = createMemo(() => { - const branch = workspaceStore.vcs?.branch - const name = branch ?? getFilename(props.directory) - return props.ctx.workspaceName(props.directory, props.project.id, branch) ?? name - }) - const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local())) - const boot = createMemo(() => open() || active()) - const count = createMemo(() => sessions()?.length ?? 0) - const hasMore = createMemo(() => workspaceStore.sessionTotal > count()) - const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) })) - const busy = createMemo(() => props.ctx.isBusy(props.directory)) - const loading = () => query.isLoading && count() === 0 - const touch = createMediaQuery("(hover: none)") - const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id))) - const loadMore = async () => { - setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5) - await globalSync.project.loadSessions(props.directory) - } - - const workspaceEditActive = createMemo(() => props.ctx.editorOpen(`workspace:${props.directory}`)) - const header = () => ( - workspaceStore.vcs?.branch} - workspaceValue={workspaceValue} - workspaceEditActive={workspaceEditActive} - InlineEditor={props.ctx.InlineEditor} - renameWorkspace={props.ctx.renameWorkspace} - setEditor={props.ctx.setEditor} - projectId={props.project.id} - /> - ) - - const openWrapper = (value: boolean) => { - props.ctx.setWorkspaceExpanded(props.directory, value) - if (value) return - if (props.ctx.editorOpen(`workspace:${props.directory}`)) props.ctx.closeEditor() - } - - createEffect(() => { - if (!boot()) return - globalSync.child(props.directory, { bootstrap: true }) - }) - - return ( -
- -
-
-
- - {header()} - - } - > -
- {header()} -
-
- menu.open} - pendingRename={() => menu.pendingRename} - setMenuOpen={(open) => setMenu("open", open)} - setPendingRename={(value) => setMenu("pendingRename", value)} - sidebarHovering={props.ctx.sidebarHovering} - touch={touch} - language={language} - workspaceValue={workspaceValue} - openEditor={props.ctx.openEditor} - showResetWorkspaceDialog={props.ctx.showResetWorkspaceDialog} - showDeleteWorkspaceDialog={props.ctx.showDeleteWorkspaceDialog} - root={props.project.worktree} - clearHoverProjectSoon={props.ctx.clearHoverProjectSoon} - navigateToNewSession={() => navigate(`/${slug()}/session`)} - /> -
-
-
- - - query.isLoading && count() === 0} - sessions={sessions} - hasMore={hasMore} - loadMore={loadMore} - language={language} - /> - -
-
- ) -} - -export const LocalWorkspace = (props: { - ctx: WorkspaceSidebarContext - project: LocalProject - sortNow: Accessor - mobile?: boolean -}): JSX.Element => { - const globalSync = useGlobalSync() - const language = useLanguage() - const workspace = createMemo(() => { - const [store, setStore] = globalSync.child(props.project.worktree) - return { store, setStore } - }) - const slug = createMemo(() => base64Encode(props.project.worktree)) - const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow())) - const count = createMemo(() => sessions()?.length ?? 0) - const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) })) - const hasMore = createMemo(() => workspace().store.sessionTotal > count()) - const loading = () => query.isLoading && count() === 0 - const loadMore = async () => { - workspace().setStore("limit", (limit) => (limit ?? 0) + 5) - await globalSync.project.loadSessions(props.project.worktree) - } - - return ( -
props.ctx.setScrollContainerRef(el, props.mobile)} - class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar [overflow-anchor:none]" - > - false} - loading={loading} - sessions={sessions} - hasMore={hasMore} - loadMore={loadMore} - language={language} - /> -
- ) -} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx deleted file mode 100644 index 787b2199735..00000000000 --- a/packages/app/src/pages/session.tsx +++ /dev/null @@ -1,1972 +0,0 @@ -import type { Project, UserMessage } from "@kilocode/sdk/v2" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query" -import { - batch, - onCleanup, - Show, - Match, - Switch, - createMemo, - createEffect, - createComputed, - on, - onMount, - untrack, - createResource, -} from "solid-js" -import { makeEventListener } from "@solid-primitives/event-listener" -import { createMediaQuery } from "@solid-primitives/media" -import { createResizeObserver } from "@solid-primitives/resize-observer" -import { useLocal } from "@/context/local" -import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file" -import { createStore } from "solid-js/store" -import { ResizeHandle } from "@opencode-ai/ui/resize-handle" -import { Select } from "@opencode-ai/ui/select" -import { Tabs } from "@opencode-ai/ui/tabs" -import { createAutoScroll } from "@opencode-ai/ui/hooks" -import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" -import { Button } from "@opencode-ai/ui/button" -import { showToast } from "@opencode-ai/ui/toast" -import { checksum } from "@opencode-ai/shared/util/encode" -import { useSearchParams } from "@solidjs/router" -import { NewSessionView, SessionHeader } from "@/components/session" -import { useComments } from "@/context/comments" -import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch" -import { useGlobalSync } from "@/context/global-sync" -import { useLanguage } from "@/context/language" -import { useLayout } from "@/context/layout" -import { usePrompt } from "@/context/prompt" -import { useSDK } from "@/context/sdk" -import { useSettings } from "@/context/settings" -import { useSync } from "@/context/sync" -import { useTerminal } from "@/context/terminal" -import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit" -import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer" -import { - createOpenReviewFile, - createSessionTabs, - createSizing, - focusTerminalById, - shouldFocusTerminalOnKeyDown, -} from "@/pages/session/helpers" -import { MessageTimeline } from "@/pages/session/message-timeline" -import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab" -import { useSessionLayout } from "@/pages/session/session-layout" -import { syncSessionModel } from "@/pages/session/session-model-helpers" -import { SessionSidePanel } from "@/pages/session/session-side-panel" -import { TerminalPanel } from "@/pages/session/terminal-panel" -import { useSessionCommands } from "@/pages/session/use-session-commands" -import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" -import { Identifier } from "@/utils/id" -import { diffs as list } from "@/utils/diffs" -import { Persist, persisted } from "@/utils/persist" -import { extractPromptFromParts } from "@/utils/prompt" -import { same } from "@/utils/same" -import { formatServerError } from "@/utils/server-errors" - -const emptyUserMessages: UserMessage[] = [] -type FollowupItem = FollowupDraft & { id: string } -type FollowupEdit = Pick -const emptyFollowups: FollowupItem[] = [] - -type ChangeMode = "git" | "branch" | "turn" -type VcsMode = "git" | "branch" - -type SessionHistoryWindowInput = { - sessionID: () => string | undefined - messagesReady: () => boolean - loaded: () => number - visibleUserMessages: () => UserMessage[] - historyMore: () => boolean - historyLoading: () => boolean - loadMore: (sessionID: string) => Promise - userScrolled: () => boolean - scroller: () => HTMLDivElement | undefined -} - -/** - * Maintains the rendered history window for a session timeline. - * - * It keeps initial paint bounded to recent turns, reveals cached turns in - * small batches while scrolling upward, and prefetches older history near top. - */ -function createSessionHistoryWindow(input: SessionHistoryWindowInput) { - const turnInit = 10 - const turnBatch = 8 - const turnScrollThreshold = 200 - const turnPrefetchBuffer = 16 - const prefetchCooldownMs = 400 - const prefetchNoGrowthLimit = 2 - - const [state, setState] = createStore({ - turnID: undefined as string | undefined, - turnStart: 0, - prefetchUntil: 0, - prefetchNoGrowth: 0, - }) - - const initialTurnStart = (len: number) => (len > turnInit ? len - turnInit : 0) - - const turnStart = createMemo(() => { - const id = input.sessionID() - const len = input.visibleUserMessages().length - if (!id || len <= 0) return 0 - if (state.turnID !== id) return initialTurnStart(len) - if (state.turnStart <= 0) return 0 - if (state.turnStart >= len) return initialTurnStart(len) - return state.turnStart - }) - - const setTurnStart = (start: number) => { - const id = input.sessionID() - const next = start > 0 ? start : 0 - if (!id) { - setState({ turnID: undefined, turnStart: next }) - return - } - setState({ turnID: id, turnStart: next }) - } - - const renderedUserMessages = createMemo( - () => { - const msgs = input.visibleUserMessages() - const start = turnStart() - if (start <= 0) return msgs - return msgs.slice(start) - }, - emptyUserMessages, - { - equals: same, - }, - ) - - const preserveScroll = (fn: () => void) => { - const el = input.scroller() - if (!el) { - fn() - return - } - const beforeTop = el.scrollTop - const beforeHeight = el.scrollHeight - fn() - requestAnimationFrame(() => { - const delta = el.scrollHeight - beforeHeight - if (!delta) return - el.scrollTop = beforeTop + delta - }) - } - - const backfillTurns = () => { - const start = turnStart() - if (start <= 0) return - - const next = start - turnBatch - const nextStart = next > 0 ? next : 0 - - preserveScroll(() => setTurnStart(nextStart)) - } - - /** Button path: reveal all cached turns, fetch older history, reveal one batch. */ - const loadAndReveal = async () => { - const id = input.sessionID() - if (!id) return - - const start = turnStart() - const beforeVisible = input.visibleUserMessages().length - let loaded = input.loaded() - - if (start > 0) setTurnStart(0) - - if (!input.historyMore() || input.historyLoading()) return - - let afterVisible = beforeVisible - let added = 0 - - while (true) { - await input.loadMore(id) - if (input.sessionID() !== id) return - - afterVisible = input.visibleUserMessages().length - const nextLoaded = input.loaded() - const raw = nextLoaded - loaded - added += raw - loaded = nextLoaded - - if (afterVisible > beforeVisible) break - if (raw <= 0) break - if (!input.historyMore()) break - } - - if (added <= 0) return - if (state.prefetchNoGrowth) setState("prefetchNoGrowth", 0) - - const growth = afterVisible - beforeVisible - if (growth <= 0) return - if (turnStart() !== 0) return - - const target = Math.min(afterVisible, beforeVisible + turnBatch) - setTurnStart(Math.max(0, afterVisible - target)) - } - - /** Scroll/prefetch path: fetch older history from server. */ - const fetchOlderMessages = async (opts?: { prefetch?: boolean }) => { - const id = input.sessionID() - if (!id) return - if (!input.historyMore() || input.historyLoading()) return - - if (opts?.prefetch) { - const now = Date.now() - if (state.prefetchUntil > now) return - if (state.prefetchNoGrowth >= prefetchNoGrowthLimit) return - setState("prefetchUntil", now + prefetchCooldownMs) - } - - const start = turnStart() - const beforeVisible = input.visibleUserMessages().length - const beforeRendered = start <= 0 ? beforeVisible : renderedUserMessages().length - let loaded = input.loaded() - let added = 0 - let growth = 0 - - while (true) { - await input.loadMore(id) - if (input.sessionID() !== id) return - - const nextLoaded = input.loaded() - const raw = nextLoaded - loaded - added += raw - loaded = nextLoaded - growth = input.visibleUserMessages().length - beforeVisible - - if (growth > 0) break - if (raw <= 0) break - if (opts?.prefetch) break - if (!input.historyMore()) break - } - - const afterVisible = input.visibleUserMessages().length - - if (opts?.prefetch) { - setState("prefetchNoGrowth", added > 0 ? 0 : state.prefetchNoGrowth + 1) - } else if (added > 0 && state.prefetchNoGrowth) { - setState("prefetchNoGrowth", 0) - } - - if (added <= 0) return - if (growth <= 0) return - - if (opts?.prefetch) { - const current = turnStart() - preserveScroll(() => setTurnStart(current + growth)) - return - } - - if (turnStart() !== start) return - - const currentRendered = renderedUserMessages().length - const base = Math.max(beforeRendered, currentRendered) - const target = Math.min(afterVisible, base + turnBatch) - preserveScroll(() => setTurnStart(Math.max(0, afterVisible - target))) - } - - const onScrollerScroll = () => { - if (!input.userScrolled()) return - const el = input.scroller() - if (!el) return - if (el.scrollTop >= turnScrollThreshold) return - - const start = turnStart() - if (start > 0) { - if (start <= turnPrefetchBuffer) { - void fetchOlderMessages({ prefetch: true }) - } - backfillTurns() - return - } - - void fetchOlderMessages() - } - - createEffect( - on( - input.sessionID, - () => { - setState({ prefetchUntil: 0, prefetchNoGrowth: 0 }) - }, - { defer: true }, - ), - ) - - createEffect( - on( - () => [input.sessionID(), input.messagesReady()] as const, - ([id, ready]) => { - if (!id || !ready) return - setTurnStart(initialTurnStart(input.visibleUserMessages().length)) - }, - { defer: true }, - ), - ) - - return { - turnStart, - setTurnStart, - renderedUserMessages, - loadAndReveal, - onScrollerScroll, - } -} - -export default function Page() { - const globalSync = useGlobalSync() - const layout = useLayout() - const local = useLocal() - const file = useFile() - const sync = useSync() - const queryClient = useQueryClient() - const dialog = useDialog() - const language = useLanguage() - const sdk = useSDK() - const settings = useSettings() - const prompt = usePrompt() - const comments = useComments() - const terminal = useTerminal() - const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>() - const { params, sessionKey, tabs, view } = useSessionLayout() - - createEffect(() => { - if (!prompt.ready()) return - untrack(() => { - if (params.id) return - const text = searchParams.prompt - if (!text) return - prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) - setSearchParams({ ...searchParams, prompt: undefined }) - }) - }) - - const [ui, setUi] = createStore({ - pendingMessage: undefined as string | undefined, - reviewSnap: false, - scrollGesture: 0, - scroll: { - overflow: false, - bottom: true, - jump: false, - }, - }) - - const composer = createSessionComposerState() - - const workspaceKey = createMemo(() => params.dir ?? "") - const workspaceTabs = createMemo(() => layout.tabs(workspaceKey)) - - createEffect( - on( - () => params.id, - (id, prev) => { - if (!id) return - if (prev) return - - const pending = layout.handoff.tabs() - if (!pending) return - if (Date.now() - pending.at > 60_000) { - layout.handoff.clearTabs() - return - } - - if (pending.id !== id) return - layout.handoff.clearTabs() - if (pending.dir !== (params.dir ?? "")) return - - const from = workspaceTabs().tabs() - if (from.all.length === 0 && !from.active) return - - const current = tabs().tabs() - if (current.all.length > 0 || current.active) return - - const all = normalizeTabs(from.all) - const active = from.active ? normalizeTab(from.active) : undefined - tabs().setAll(all) - tabs().setActive(active && all.includes(active) ? active : all[0]) - - workspaceTabs().setAll([]) - workspaceTabs().setActive(undefined) - }, - { defer: true }, - ), - ) - - const isDesktop = createMediaQuery("(min-width: 768px)") - const size = createSizing() - const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) - const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened()) - const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen()) - const sessionPanelWidth = createMemo(() => { - if (!desktopSidePanelOpen()) return "100%" - if (desktopReviewOpen()) return `${layout.session.width()}px` - return `calc(100% - ${layout.fileTree.width()}px)` - }) - const centered = createMemo(() => isDesktop() && !desktopReviewOpen()) - - function normalizeTab(tab: string) { - if (!tab.startsWith("file://")) return tab - return file.tab(tab) - } - - function normalizeTabs(list: string[]) { - const seen = new Set() - const next: string[] = [] - for (const item of list) { - const value = normalizeTab(item) - if (seen.has(value)) continue - seen.add(value) - next.push(value) - } - return next - } - - const openReviewPanel = () => { - if (!view().reviewPanel.opened()) view().reviewPanel.open() - } - - const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) - const isChildSession = createMemo(() => !!info()?.parentID) - const diffs = createMemo(() => (params.id ? list(sync.data.session_diff[params.id]) : [])) - const canReview = createMemo(() => !!sync.project) - const reviewTab = createMemo(() => isDesktop()) - const tabState = createSessionTabs({ - tabs, - pathFromTab: file.pathFromTab, - normalizeTab, - review: reviewTab, - hasReview: canReview, - }) - const activeTab = tabState.activeTab - const activeFileTab = tabState.activeFileTab - const revertMessageID = createMemo(() => info()?.revert?.messageID) - const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) - const messagesReady = createMemo(() => { - const id = params.id - if (!id) return true - return sync.data.message[id] !== undefined - }) - const historyMore = createMemo(() => { - const id = params.id - if (!id) return false - return sync.session.history.more(id) - }) - const historyLoading = createMemo(() => { - const id = params.id - if (!id) return false - return sync.session.history.loading(id) - }) - const userMessages = createMemo( - () => messages().filter((m) => m.role === "user") as UserMessage[], - emptyUserMessages, - { equals: same }, - ) - const visibleUserMessages = createMemo( - () => { - const revert = revertMessageID() - if (!revert) return userMessages() - return userMessages().filter((m) => m.id < revert) - }, - emptyUserMessages, - { - equals: same, - }, - ) - const lastUserMessage = createMemo(() => visibleUserMessages().at(-1)) - - createEffect(() => { - const tab = activeFileTab() - if (!tab) return - - const path = file.pathFromTab(tab) - if (path) void file.load(path) - }) - - createEffect( - on( - () => lastUserMessage()?.id, - () => { - const msg = lastUserMessage() - if (!msg) return - syncSessionModel(local, msg) - }, - ), - ) - - createEffect( - on( - () => ({ dir: params.dir, id: params.id }), - (next, prev) => { - if (!prev) return - if (next.dir === prev.dir && next.id === prev.id) return - if (prev.id && !next.id) local.session.reset() - }, - { defer: true }, - ), - ) - - const [store, setStore] = createStore({ - messageId: undefined as string | undefined, - mobileTab: "session" as "session" | "changes", - changes: "git" as ChangeMode, - newSessionWorktree: "main", - deferRender: false, - }) - - const [followup, setFollowup] = persisted( - Persist.workspace(sdk.directory, "followup", ["followup.v1"]), - createStore<{ - items: Record - failed: Record - paused: Record - edit: Record - }>({ - items: {}, - failed: {}, - paused: {}, - edit: {}, - }), - ) - - createComputed((prev) => { - const key = sessionKey() - if (key !== prev) { - setStore("deferRender", true) - requestAnimationFrame(() => { - setTimeout(() => setStore("deferRender", false), 0) - }) - } - return key - }, sessionKey()) - - let reviewFrame: number | undefined - let refreshFrame: number | undefined - let refreshTimer: number | undefined - let todoFrame: number | undefined - let todoTimer: number | undefined - let diffFrame: number | undefined - let diffTimer: number | undefined - - createComputed((prev) => { - const open = desktopReviewOpen() - if (prev === undefined || prev === open) return open - - if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame) - setUi("reviewSnap", true) - reviewFrame = requestAnimationFrame(() => { - reviewFrame = undefined - setUi("reviewSnap", false) - }) - return open - }, desktopReviewOpen()) - - const turnDiffs = createMemo(() => list(lastUserMessage()?.summary?.diffs)) - const nogit = createMemo(() => !!sync.project && sync.project.vcs !== "git") - const changesOptions = createMemo(() => { - const list: ChangeMode[] = [] - if (sync.project?.vcs === "git") list.push("git") - if ( - sync.project?.vcs === "git" && - sync.data.vcs?.branch && - sync.data.vcs?.default_branch && - sync.data.vcs.branch !== sync.data.vcs.default_branch - ) { - list.push("branch") - } - list.push("turn") - return list - }) - const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") - const wantsReview = createMemo(() => - isDesktop() - ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review") - : store.mobileTab === "changes", - ) - const vcsMode = createMemo(() => { - if (store.changes === "git" || store.changes === "branch") return store.changes - }) - const vcsKey = createMemo( - () => ["session-vcs", sdk.directory, sync.data.vcs?.branch ?? "", sync.data.vcs?.default_branch ?? ""] as const, - ) - const vcsQuery = createQuery(() => { - const mode = vcsMode() - const enabled = wantsReview() && sync.project?.vcs === "git" - - return { - queryKey: [...vcsKey(), mode] as const, - enabled, - staleTime: Number.POSITIVE_INFINITY, - gcTime: 60 * 1000, - queryFn: mode - ? () => - sdk.client.vcs - .diff({ mode }) - .then((result) => list(result.data)) - .catch((error) => { - console.debug("[session-review] failed to load vcs diff", { mode, error }) - return [] - }) - : skipToken, - } - }) - const refreshVcs = () => void queryClient.invalidateQueries({ queryKey: vcsKey() }) - const reviewDiffs = () => { - if (store.changes === "git" || store.changes === "branch") - // avoids suspense - return vcsQuery.isFetched ? (vcsQuery.data ?? []) : [] - return turnDiffs() - } - const reviewCount = () => reviewDiffs().length - const hasReview = () => reviewCount() > 0 - const reviewReady = () => { - if (store.changes === "git" || store.changes === "branch") return !vcsQuery.isPending - return true - } - - const newSessionWorktree = createMemo(() => { - if (store.newSessionWorktree === "create") return "create" - const project = sync.project - if (project && sdk.directory !== project.worktree) return sdk.directory - return "main" - }) - - const setActiveMessage = (message: UserMessage | undefined) => { - messageMark = scrollMark - setStore("messageId", message?.id) - } - - const anchor = (id: string) => `message-${id}` - - const cursor = () => { - const root = scroller - if (!root) return store.messageId - - const box = root.getBoundingClientRect() - const line = box.top + 100 - const list = [...root.querySelectorAll("[data-message-id]")] - .map((el) => { - const id = el.dataset.messageId - if (!id) return - - const rect = el.getBoundingClientRect() - return { id, top: rect.top, bottom: rect.bottom } - }) - .filter((item): item is { id: string; top: number; bottom: number } => !!item) - - const shown = list.filter((item) => item.bottom > box.top && item.top < box.bottom) - const hit = shown.find((item) => item.top <= line && item.bottom >= line) - if (hit) return hit.id - - const near = [...shown].sort((a, b) => { - const da = Math.abs(a.top - line) - const db = Math.abs(b.top - line) - if (da !== db) return da - db - return a.top - b.top - })[0] - if (near) return near.id - - return list.filter((item) => item.top <= line).at(-1)?.id ?? list[0]?.id ?? store.messageId - } - - function navigateMessageByOffset(offset: number) { - const msgs = visibleUserMessages() - if (msgs.length === 0) return - - const current = store.messageId && messageMark === scrollMark ? store.messageId : cursor() - const base = current ? msgs.findIndex((m) => m.id === current) : msgs.length - const currentIndex = base === -1 ? msgs.length : base - const targetIndex = currentIndex + offset - if (targetIndex < 0 || targetIndex > msgs.length) return - - if (targetIndex === msgs.length) { - resumeScroll() - return - } - - autoScroll.pause() - scrollToMessage(msgs[targetIndex], "auto") - } - - function upsert(next: Project) { - const list = globalSync.data.project - sync.set("project", next.id) - const idx = list.findIndex((item) => item.id === next.id) - if (idx >= 0) { - globalSync.set( - "project", - list.map((item, i) => (i === idx ? { ...item, ...next } : item)), - ) - return - } - const at = list.findIndex((item) => item.id > next.id) - if (at >= 0) { - globalSync.set("project", [...list.slice(0, at), next, ...list.slice(at)]) - return - } - globalSync.set("project", [...list, next]) - } - - const gitMutation = useMutation(() => ({ - mutationFn: () => sdk.client.project.initGit(), - onSuccess: (x) => { - if (!x.data) return - upsert(x.data) - }, - onError: (err) => { - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: formatServerError(err, language.t), - }) - }, - })) - - function initGit() { - if (gitMutation.isPending) return - gitMutation.mutate() - } - - let inputRef!: HTMLDivElement - let promptDock: HTMLDivElement | undefined - let dockHeight = 0 - let scroller: HTMLDivElement | undefined - let content: HTMLDivElement | undefined - let scrollMark = 0 - let messageMark = 0 - - const scrollGestureWindowMs = 250 - - const markScrollGesture = (target?: EventTarget | null) => { - const root = scroller - if (!root) return - - const el = target instanceof Element ? target : undefined - const nested = el?.closest("[data-scrollable]") - if (nested && nested !== root) return - - setUi("scrollGesture", Date.now()) - } - - const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs - - const [sessionSync] = createResource( - () => [sdk.directory, params.id] as const, - ([directory, id]) => { - if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame) - if (refreshTimer !== undefined) window.clearTimeout(refreshTimer) - refreshFrame = undefined - refreshTimer = undefined - if (!id) return - - const cached = untrack(() => sync.data.message[id] !== undefined) - const stale = !cached - ? false - : (() => { - const info = getSessionPrefetch(directory, id) - if (!info) return true - return Date.now() - info.at > SESSION_PREFETCH_TTL - })() - - refreshFrame = requestAnimationFrame(() => { - refreshFrame = undefined - refreshTimer = window.setTimeout(() => { - refreshTimer = undefined - if (params.id !== id) return - untrack(() => { - if (stale) void sync.session.sync(id, { force: true }) - }) - }, 0) - }) - - return sync.session.sync(id) - }, - ) - - createEffect( - on( - () => { - const id = params.id - return [ - sdk.directory, - id, - id ? (sync.data.session_status[id]?.type ?? "idle") : "idle", - id ? composer.blocked() : false, - ] as const - }, - ([dir, id, status, blocked]) => { - if (todoFrame !== undefined) cancelAnimationFrame(todoFrame) - if (todoTimer !== undefined) window.clearTimeout(todoTimer) - todoFrame = undefined - todoTimer = undefined - if (!id) return - if (status === "idle" && !blocked) return - const cached = untrack(() => sync.data.todo[id] !== undefined || globalSync.data.session_todo[id] !== undefined) - - todoFrame = requestAnimationFrame(() => { - todoFrame = undefined - todoTimer = window.setTimeout(() => { - todoTimer = undefined - if (sdk.directory !== dir || params.id !== id) return - untrack(() => { - void sync.session.todo(id, cached ? { force: true } : undefined) - }) - }, 0) - }) - }, - { defer: true }, - ), - ) - - createEffect( - on( - () => visibleUserMessages().at(-1)?.id, - (lastId, prevLastId) => { - if (lastId && prevLastId && lastId > prevLastId) { - setStore("messageId", undefined) - } - }, - { defer: true }, - ), - ) - - createEffect( - on( - sessionKey, - () => { - setStore("messageId", undefined) - setStore("changes", "git") - setUi("pendingMessage", undefined) - }, - { defer: true }, - ), - ) - - const stopVcs = sdk.event.listen((evt) => { - if (evt.details.type !== "file.watcher.updated") return - const props = - typeof evt.details.properties === "object" && evt.details.properties - ? (evt.details.properties as Record) - : undefined - const file = typeof props?.file === "string" ? props.file : undefined - if (!file || file.startsWith(".git/")) return - refreshVcs() - }) - onCleanup(stopVcs) - - createEffect( - on( - () => params.dir, - (dir) => { - if (!dir) return - setStore("newSessionWorktree", "main") - }, - { defer: true }, - ), - ) - - const selectionPreview = (path: string, selection: FileSelection) => { - const content = file.get(path)?.content?.content - if (!content) return undefined - return previewSelectedLines(content, { start: selection.startLine, end: selection.endLine }) - } - - const addCommentToContext = (input: { - file: string - selection: SelectedLineRange - comment: string - preview?: string - origin?: "review" | "file" - }) => { - const selection = selectionFromLines(input.selection) - const preview = input.preview ?? selectionPreview(input.file, selection) - const saved = comments.add({ - file: input.file, - selection: input.selection, - comment: input.comment, - }) - prompt.context.add({ - type: "file", - path: input.file, - selection, - comment: input.comment, - commentID: saved.id, - commentOrigin: input.origin, - preview, - }) - } - - const updateCommentInContext = (input: { - id: string - file: string - selection: SelectedLineRange - comment: string - preview?: string - }) => { - comments.update(input.file, input.id, input.comment) - prompt.context.updateComment(input.file, input.id, { - comment: input.comment, - ...(input.preview ? { preview: input.preview } : {}), - }) - } - - const removeCommentFromContext = (input: { id: string; file: string }) => { - comments.remove(input.file, input.id) - prompt.context.removeComment(input.file, input.id) - } - - const reviewCommentActions = createMemo(() => ({ - moreLabel: language.t("common.moreOptions"), - editLabel: language.t("common.edit"), - deleteLabel: language.t("common.delete"), - saveLabel: language.t("common.save"), - })) - - const isEditableTarget = (target: EventTarget | null | undefined) => { - if (!(target instanceof HTMLElement)) return false - return /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(target.tagName) || target.isContentEditable - } - - const deepActiveElement = () => { - let current: Element | null = document.activeElement - while (current instanceof HTMLElement && current.shadowRoot?.activeElement) { - current = current.shadowRoot.activeElement - } - return current instanceof HTMLElement ? current : undefined - } - - const handleKeyDown = (event: KeyboardEvent) => { - const path = event.composedPath() - const target = path.find((item): item is HTMLElement => item instanceof HTMLElement) - const activeElement = deepActiveElement() - - const protectedTarget = path.some( - (item) => item instanceof HTMLElement && item.closest("[data-prevent-autofocus]") !== null, - ) - if (protectedTarget || isEditableTarget(target)) return - - if (activeElement) { - const isProtected = activeElement.closest("[data-prevent-autofocus]") - const isInput = isEditableTarget(activeElement) - if (isProtected || isInput) return - } - if (dialog.active) return - - if (activeElement === inputRef) { - if (event.key === "Escape") inputRef?.blur() - return - } - - // Prefer the open terminal over the composer when it can take focus - if (view().terminal.opened()) { - const id = terminal.active() - if (id && shouldFocusTerminalOnKeyDown(event) && focusTerminalById(id)) return - } - - // Only treat explicit scroll keys as potential "user scroll" gestures. - if (event.key === "PageUp" || event.key === "PageDown" || event.key === "Home" || event.key === "End") { - markScrollGesture() - return - } - - if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) { - if (composer.blocked() || isChildSession()) return - inputRef?.focus() - } - } - - createEffect(() => { - const list = changesOptions() - if (list.includes(store.changes)) return - const next = list[0] - if (!next) return - setStore("changes", next) - }) - - createEffect( - on( - () => sync.data.session_status[params.id ?? ""]?.type, - (next, prev) => { - if (next !== "idle" || prev === undefined || prev === "idle") return - refreshVcs() - }, - { defer: true }, - ), - ) - - const fileTreeTab = () => layout.fileTree.tab() - const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value) - - const [tree, setTree] = createStore({ - reviewScroll: undefined as HTMLDivElement | undefined, - pendingDiff: undefined as string | undefined, - activeDiff: undefined as string | undefined, - }) - - createEffect( - on( - sessionKey, - () => { - setTree({ - reviewScroll: undefined, - pendingDiff: undefined, - activeDiff: undefined, - }) - }, - { defer: true }, - ), - ) - - const showAllFiles = () => { - if (fileTreeTab() !== "changes") return - setFileTreeTab("all") - } - - const focusInput = () => { - if (isChildSession()) return - inputRef?.focus() - } - - useSessionCommands({ - navigateMessageByOffset, - setActiveMessage, - focusInput, - review: reviewTab, - }) - - const openReviewFile = createOpenReviewFile({ - showAllFiles, - tabForPath: file.tab, - openTab: tabs().open, - setActive: tabs().setActive, - loadFile: file.load, - }) - - const changesTitle = () => { - if (!canReview()) { - return null - } - - const label = (option: ChangeMode) => { - if (option === "git") return language.t("ui.sessionReview.title.git") - if (option === "branch") return language.t("ui.sessionReview.title.branch") - return language.t("ui.sessionReview.title.lastTurn") - } - - return ( -
` - - const focused = focusTerminalById("one") - - expect(focused).toBe(true) - expect(document.activeElement?.tagName).toBe("TEXTAREA") - }) - - test("falls back to terminal element focus", () => { - document.body.innerHTML = `
` - const terminal = document.querySelector('[data-component="terminal"]') as HTMLElement - let pointerDown = false - terminal.addEventListener("pointerdown", () => { - pointerDown = true - }) - - const focused = focusTerminalById("two") - - expect(focused).toBe(true) - expect(document.activeElement).toBe(terminal) - expect(pointerDown).toBe(true) - }) -}) - -describe("shouldFocusTerminalOnKeyDown", () => { - test("skips pure modifier keys", () => { - expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "Meta", metaKey: true }))).toBe(false) - expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "Control", ctrlKey: true }))).toBe(false) - expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "Alt", altKey: true }))).toBe(false) - expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "Shift", shiftKey: true }))).toBe(false) - }) - - test("skips shortcut key combos", () => { - expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "c", metaKey: true }))).toBe(false) - expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "c", ctrlKey: true }))).toBe(false) - expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "ArrowLeft", altKey: true }))).toBe(false) - }) - - test("keeps plain typing focused on terminal", () => { - expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "a" }))).toBe(true) - expect(shouldFocusTerminalOnKeyDown(new KeyboardEvent("keydown", { key: "A", shiftKey: true }))).toBe(true) - }) -}) - -describe("getTabReorderIndex", () => { - test("returns target index for valid drag reorder", () => { - expect(getTabReorderIndex(["a", "b", "c"], "a", "c")).toBe(2) - }) - - test("returns undefined for unknown droppable id", () => { - expect(getTabReorderIndex(["a", "b", "c"], "a", "missing")).toBeUndefined() - }) -}) - -describe("createSessionTabs", () => { - test("normalizes the effective file tab", () => { - createRoot((dispose) => { - const [state] = createStore({ - active: undefined as string | undefined, - all: ["file://src/a.ts", "context"], - }) - const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all })) - const result = createSessionTabs({ - tabs, - pathFromTab: (tab) => (tab.startsWith("file://") ? tab.slice("file://".length) : undefined), - normalizeTab: (tab) => (tab.startsWith("file://") ? `norm:${tab.slice("file://".length)}` : tab), - }) - - expect(result.activeTab()).toBe("norm:src/a.ts") - expect(result.activeFileTab()).toBe("norm:src/a.ts") - expect(result.closableTab()).toBe("norm:src/a.ts") - dispose() - }) - }) - - test("prefers context and review fallbacks when no file tab is active", () => { - createRoot((dispose) => { - const [state] = createStore({ - active: undefined as string | undefined, - all: ["context"], - }) - const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all })) - const result = createSessionTabs({ - tabs, - pathFromTab: () => undefined, - normalizeTab: (tab) => tab, - review: () => true, - hasReview: () => true, - }) - - expect(result.activeTab()).toBe("context") - expect(result.closableTab()).toBe("context") - dispose() - }) - - createRoot((dispose) => { - const [state] = createStore({ - active: undefined as string | undefined, - all: [], - }) - const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all })) - const result = createSessionTabs({ - tabs, - pathFromTab: () => undefined, - normalizeTab: (tab) => tab, - review: () => true, - hasReview: () => true, - }) - - expect(result.activeTab()).toBe("review") - expect(result.activeFileTab()).toBeUndefined() - expect(result.closableTab()).toBeUndefined() - dispose() - }) - }) -}) diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts deleted file mode 100644 index e136ba9991b..00000000000 --- a/packages/app/src/pages/session/helpers.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js" -import { createStore } from "solid-js/store" -import { makeEventListener } from "@solid-primitives/event-listener" -import { same } from "@/utils/same" - -const emptyTabs: string[] = [] - -type Tabs = { - active: Accessor - all: Accessor -} - -type TabsInput = { - tabs: Accessor - pathFromTab: (tab: string) => string | undefined - normalizeTab: (tab: string) => string - review?: Accessor - hasReview?: Accessor -} - -export const getSessionKey = (dir: string | undefined, id: string | undefined) => `${dir ?? ""}${id ? `/${id}` : ""}` - -export const createSessionTabs = (input: TabsInput) => { - const review = input.review ?? (() => false) - const hasReview = input.hasReview ?? (() => false) - const contextOpen = createMemo(() => input.tabs().active() === "context" || input.tabs().all().includes("context")) - const openedTabs = createMemo( - () => { - const seen = new Set() - return input - .tabs() - .all() - .flatMap((tab) => { - if (tab === "context" || tab === "review") return [] - const value = input.pathFromTab(tab) ? input.normalizeTab(tab) : tab - if (seen.has(value)) return [] - seen.add(value) - return [value] - }) - }, - emptyTabs, - { equals: same }, - ) - const activeTab = createMemo(() => { - const active = input.tabs().active() - if (active === "context") return active - if (active === "review" && review()) return active - if (active && input.pathFromTab(active)) return input.normalizeTab(active) - - const first = openedTabs()[0] - if (first) return first - if (contextOpen()) return "context" - if (review() && hasReview()) return "review" - return "empty" - }) - const activeFileTab = createMemo(() => { - const active = activeTab() - if (!openedTabs().includes(active)) return - return active - }) - const closableTab = createMemo(() => { - const active = activeTab() - if (active === "context") return active - if (!openedTabs().includes(active)) return - return active - }) - - return { - contextOpen, - openedTabs, - activeTab, - activeFileTab, - closableTab, - } -} - -export const focusTerminalById = (id: string) => { - const wrapper = document.getElementById(`terminal-wrapper-${id}`) - const terminal = wrapper?.querySelector('[data-component="terminal"]') - if (!(terminal instanceof HTMLElement)) return false - - const textarea = terminal.querySelector("textarea") - if (textarea instanceof HTMLTextAreaElement) { - textarea.focus() - return true - } - - terminal.focus() - terminal.dispatchEvent( - typeof PointerEvent === "function" - ? new PointerEvent("pointerdown", { bubbles: true, cancelable: true }) - : new MouseEvent("pointerdown", { bubbles: true, cancelable: true }), - ) - return true -} - -const skip = new Set(["Alt", "Control", "Meta", "Shift"]) - -export const shouldFocusTerminalOnKeyDown = (event: Pick) => { - if (skip.has(event.key)) return false - return !(event.ctrlKey || event.metaKey || event.altKey) -} - -export const createOpenReviewFile = (input: { - showAllFiles: () => void - tabForPath: (path: string) => string - openTab: (tab: string) => void - setActive: (tab: string) => void - loadFile: (path: string) => any | Promise -}) => { - return (path: string) => { - batch(() => { - input.showAllFiles() - const maybePromise = input.loadFile(path) - const open = () => { - const tab = input.tabForPath(path) - input.openTab(tab) - input.setActive(tab) - } - if (maybePromise instanceof Promise) void maybePromise.then(open) - else open() - }) - } -} - -export const createOpenSessionFileTab = (input: { - normalizeTab: (tab: string) => string - openTab: (tab: string) => void - pathFromTab: (tab: string) => string | undefined - loadFile: (path: string) => void - openReviewPanel: () => void - setActive: (tab: string) => void -}) => { - return (value: string) => { - const next = input.normalizeTab(value) - input.openTab(next) - - const path = input.pathFromTab(next) - if (!path) return - - input.loadFile(path) - input.openReviewPanel() - input.setActive(next) - } -} - -export const getTabReorderIndex = (tabs: readonly string[], from: string, to: string) => { - const fromIndex = tabs.indexOf(from) - const toIndex = tabs.indexOf(to) - if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return undefined - return toIndex -} - -export const createSizing = () => { - const [state, setState] = createStore({ active: false }) - let t: number | undefined - - const stop = () => { - if (t !== undefined) { - clearTimeout(t) - t = undefined - } - setState("active", false) - } - - const start = () => { - if (t !== undefined) { - clearTimeout(t) - t = undefined - } - setState("active", true) - } - - onMount(() => { - makeEventListener(window, "pointerup", stop) - makeEventListener(window, "pointercancel", stop) - makeEventListener(window, "blur", stop) - }) - - onCleanup(() => { - if (t !== undefined) clearTimeout(t) - }) - - return { - active: () => state.active, - start, - touch() { - start() - t = window.setTimeout(stop, 120) - }, - } -} - -export type Sizing = ReturnType diff --git a/packages/app/src/pages/session/message-gesture.test.ts b/packages/app/src/pages/session/message-gesture.test.ts deleted file mode 100644 index b2af4bb8342..00000000000 --- a/packages/app/src/pages/session/message-gesture.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { normalizeWheelDelta, shouldMarkBoundaryGesture } from "./message-gesture" - -describe("normalizeWheelDelta", () => { - test("converts line mode to px", () => { - expect(normalizeWheelDelta({ deltaY: 3, deltaMode: 1, rootHeight: 500 })).toBe(120) - }) - - test("converts page mode to container height", () => { - expect(normalizeWheelDelta({ deltaY: -1, deltaMode: 2, rootHeight: 600 })).toBe(-600) - }) - - test("keeps pixel mode unchanged", () => { - expect(normalizeWheelDelta({ deltaY: 16, deltaMode: 0, rootHeight: 600 })).toBe(16) - }) -}) - -describe("shouldMarkBoundaryGesture", () => { - test("marks when nested scroller cannot scroll", () => { - expect( - shouldMarkBoundaryGesture({ - delta: 20, - scrollTop: 0, - scrollHeight: 300, - clientHeight: 300, - }), - ).toBe(true) - }) - - test("marks when scrolling beyond top boundary", () => { - expect( - shouldMarkBoundaryGesture({ - delta: -40, - scrollTop: 10, - scrollHeight: 1000, - clientHeight: 400, - }), - ).toBe(true) - }) - - test("marks when scrolling beyond bottom boundary", () => { - expect( - shouldMarkBoundaryGesture({ - delta: 50, - scrollTop: 580, - scrollHeight: 1000, - clientHeight: 400, - }), - ).toBe(true) - }) - - test("does not mark when nested scroller can consume movement", () => { - expect( - shouldMarkBoundaryGesture({ - delta: 20, - scrollTop: 200, - scrollHeight: 1000, - clientHeight: 400, - }), - ).toBe(false) - }) -}) diff --git a/packages/app/src/pages/session/message-gesture.ts b/packages/app/src/pages/session/message-gesture.ts deleted file mode 100644 index 731cb1bdeb6..00000000000 --- a/packages/app/src/pages/session/message-gesture.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const normalizeWheelDelta = (input: { deltaY: number; deltaMode: number; rootHeight: number }) => { - if (input.deltaMode === 1) return input.deltaY * 40 - if (input.deltaMode === 2) return input.deltaY * input.rootHeight - return input.deltaY -} - -export const shouldMarkBoundaryGesture = (input: { - delta: number - scrollTop: number - scrollHeight: number - clientHeight: number -}) => { - const max = input.scrollHeight - input.clientHeight - if (max <= 1) return true - if (!input.delta) return false - - if (input.delta < 0) return input.scrollTop + input.delta <= 0 - - const remaining = max - input.scrollTop - return input.delta > remaining -} diff --git a/packages/app/src/pages/session/message-id-from-hash.ts b/packages/app/src/pages/session/message-id-from-hash.ts deleted file mode 100644 index 2857f4b01d6..00000000000 --- a/packages/app/src/pages/session/message-id-from-hash.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const messageIdFromHash = (hash: string) => { - const value = hash.startsWith("#") ? hash.slice(1) : hash - const match = value.match(/^message-(.+)$/) - if (!match) return - return match[1] -} diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx deleted file mode 100644 index 90e74e90a88..00000000000 --- a/packages/app/src/pages/session/message-timeline.tsx +++ /dev/null @@ -1,1118 +0,0 @@ -import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX, createSignal } from "solid-js" -import { createStore, produce } from "solid-js/store" -import { useNavigate } from "@solidjs/router" -import { useMutation } from "@tanstack/solid-query" -import { Button } from "@opencode-ai/ui/button" -import { FileIcon } from "@opencode-ai/ui/file-icon" -import { Icon } from "@opencode-ai/ui/icon" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" -import { Dialog } from "@opencode-ai/ui/dialog" -import { InlineInput } from "@opencode-ai/ui/inline-input" -import { Spinner } from "@opencode-ai/ui/spinner" -import { SessionTurn } from "@opencode-ai/ui/session-turn" -import { ScrollView } from "@opencode-ai/ui/scroll-view" -import { TextField } from "@opencode-ai/ui/text-field" -import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@kilocode/sdk/v2" -import { showToast } from "@opencode-ai/ui/toast" -import { Binary } from "@opencode-ai/shared/util/binary" -import { getFilename } from "@opencode-ai/shared/util/path" -import { Popover as KobaltePopover } from "@kobalte/core/popover" -import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture" -import { SessionContextUsage } from "@/components/session-context-usage" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { createResizeObserver } from "@solid-primitives/resize-observer" -import { useLanguage } from "@/context/language" -import { useSessionKey } from "@/pages/session/session-layout" -import { useGlobalSDK } from "@/context/global-sdk" -import { usePlatform } from "@/context/platform" -import { useSettings } from "@/context/settings" -import { useSDK } from "@/context/sdk" -import { useSync } from "@/context/sync" -import { messageAgentColor } from "@/utils/agent" -import { sessionTitle } from "@/utils/session-title" -import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note" -import { makeTimer } from "@solid-primitives/timer" - -type MessageComment = { - path: string - comment: string - selection?: { - startLine: number - endLine: number - } -} - -const emptyMessages: MessageType[] = [] -const idle = { type: "idle" as const } -type UserActions = { - fork?: (input: { sessionID: string; messageID: string }) => Promise | void - revert?: (input: { sessionID: string; messageID: string }) => Promise | void -} - -const messageComments = (parts: Part[]): MessageComment[] => - parts.flatMap((part) => { - if (part.type !== "text" || !(part as TextPart).synthetic) return [] - const next = readCommentMetadata(part.metadata) ?? parseCommentNote(part.text) - if (!next) return [] - return [ - { - path: next.path, - comment: next.comment, - selection: next.selection - ? { - startLine: next.selection.startLine, - endLine: next.selection.endLine, - } - : undefined, - }, - ] - }) - -const taskDescription = (part: Part, sessionID: string) => { - if (part.type !== "tool" || part.tool !== "task") return - const metadata = "metadata" in part.state ? part.state.metadata : undefined - if (metadata?.sessionId !== sessionID) return - const value = part.state.input?.description - if (typeof value === "string" && value) return value -} - -const pace = (width: number) => Math.round(Math.max(1200, Math.min(3200, (Math.max(width, 360) * 2000) / 900))) - -const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => { - const current = target instanceof Element ? target : undefined - const nested = current?.closest("[data-scrollable]") - if (!nested || nested === root) return root - if (!(nested instanceof HTMLElement)) return root - return nested -} - -const markBoundaryGesture = (input: { - root: HTMLDivElement - target: EventTarget | null - delta: number - onMarkScrollGesture: (target?: EventTarget | null) => void -}) => { - const target = boundaryTarget(input.root, input.target) - if (target === input.root) { - input.onMarkScrollGesture(input.root) - return - } - if ( - shouldMarkBoundaryGesture({ - delta: input.delta, - scrollTop: target.scrollTop, - scrollHeight: target.scrollHeight, - clientHeight: target.clientHeight, - }) - ) { - input.onMarkScrollGesture(input.root) - } -} - -type StageConfig = { - init: number - batch: number -} - -type TimelineStageInput = { - sessionKey: () => string - turnStart: () => number - messages: () => UserMessage[] - config: StageConfig -} - -/** - * Defer-mounts small timeline windows so revealing older turns does not - * block first paint with a large DOM mount. - * - * Once staging completes for a session it never re-stages — backfill and - * new messages render immediately. - */ -function createTimelineStaging(input: TimelineStageInput) { - const [state, setState] = createStore({ - activeSession: "", - completedSession: "", - count: 0, - }) - - const stagedCount = createMemo(() => { - const total = input.messages().length - if (input.turnStart() <= 0) return total - if (state.completedSession === input.sessionKey()) return total - const init = Math.min(total, input.config.init) - if (state.count <= init) return init - if (state.count >= total) return total - return state.count - }) - - const stagedUserMessages = createMemo(() => { - const list = input.messages() - const count = stagedCount() - if (count >= list.length) return list - return list.slice(Math.max(0, list.length - count)) - }) - - let frame: number | undefined - const cancel = () => { - if (frame === undefined) return - cancelAnimationFrame(frame) - frame = undefined - } - - createEffect( - on( - () => [input.sessionKey(), input.turnStart() > 0, input.messages().length] as const, - ([sessionKey, isWindowed, total]) => { - cancel() - const shouldStage = - isWindowed && - total > input.config.init && - state.completedSession !== sessionKey && - state.activeSession !== sessionKey - if (!shouldStage) { - setState({ activeSession: "", count: total }) - return - } - - let count = Math.min(total, input.config.init) - setState({ activeSession: sessionKey, count }) - - const step = () => { - if (input.sessionKey() !== sessionKey) { - frame = undefined - return - } - const currentTotal = input.messages().length - count = Math.min(currentTotal, count + input.config.batch) - setState("count", count) - if (count >= currentTotal) { - setState({ completedSession: sessionKey, activeSession: "" }) - frame = undefined - return - } - frame = requestAnimationFrame(step) - } - frame = requestAnimationFrame(step) - }, - ), - ) - - const isStaging = createMemo(() => { - const key = input.sessionKey() - return state.activeSession === key && state.completedSession !== key - }) - - onCleanup(cancel) - return { messages: stagedUserMessages, isStaging } -} - -export function MessageTimeline(props: { - mobileChanges: boolean - mobileFallback: JSX.Element - actions?: UserActions - scroll: { overflow: boolean; bottom: boolean; jump: boolean } - onResumeScroll: () => void - setScrollRef: (el: HTMLDivElement | undefined) => void - onScheduleScrollState: (el: HTMLDivElement) => void - onAutoScrollHandleScroll: () => void - onMarkScrollGesture: (target?: EventTarget | null) => void - hasScrollGesture: () => boolean - onUserScroll: () => void - onTurnBackfillScroll: () => void - onAutoScrollInteraction: (event: MouseEvent) => void - centered: boolean - setContentRef: (el: HTMLDivElement) => void - turnStart: number - historyMore: boolean - historyLoading: boolean - onLoadEarlier: () => void - renderedUserMessages: UserMessage[] - anchor: (id: string) => string -}) { - let touchGesture: number | undefined - - const navigate = useNavigate() - const globalSDK = useGlobalSDK() - const sdk = useSDK() - const sync = useSync() - const settings = useSettings() - const dialog = useDialog() - const language = useLanguage() - const { params, sessionKey } = useSessionKey() - const platform = usePlatform() - - const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id)) - const sessionID = createMemo(() => params.id) - const sessionMessages = createMemo(() => { - const id = sessionID() - if (!id) return emptyMessages - return sync.data.message[id] ?? emptyMessages - }) - const pending = createMemo(() => - sessionMessages().findLast( - (item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number", - ), - ) - const sessionStatus = createMemo(() => { - const id = sessionID() - if (!id) return idle - return sync.data.session_status[id] ?? idle - }) - const working = createMemo(() => sessionStatus().type !== "idle") - const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent)) - - const [timeoutDone, setTimeoutDone] = createSignal(true) - - const workingStatus = createMemo<"hidden" | "showing" | "hiding">((prev) => { - if (working()) return "showing" - if (prev === "showing" || !timeoutDone()) return "hiding" - return "hidden" - }) - - createEffect(() => { - if (workingStatus() !== "hiding") return - - setTimeoutDone(false) - makeTimer(() => setTimeoutDone(true), 260, setTimeout) - }) - - const activeMessageID = createMemo(() => { - const parentID = pending()?.parentID - if (parentID) { - const messages = sessionMessages() - const result = Binary.search(messages, parentID, (message) => message.id) - const message = result.found ? messages[result.index] : messages.find((item) => item.id === parentID) - if (message && message.role === "user") return message.id - } - - const status = sessionStatus() - if (status.type !== "idle") { - const messages = sessionMessages() - for (let i = messages.length - 1; i >= 0; i--) { - if (messages[i].role === "user") return messages[i].id - } - } - - return undefined - }) - const info = createMemo(() => { - const id = sessionID() - if (!id) return - return sync.session.get(id) - }) - const titleValue = createMemo(() => info()?.title) - const titleLabel = createMemo(() => sessionTitle(titleValue())) - const shareUrl = createMemo(() => info()?.share?.url) - const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") - const parentID = createMemo(() => info()?.parentID) - const parent = createMemo(() => { - const id = parentID() - if (!id) return - return sync.session.get(id) - }) - const parentMessages = createMemo(() => { - const id = parentID() - if (!id) return emptyMessages - return sync.data.message[id] ?? emptyMessages - }) - const parentTitle = createMemo(() => sessionTitle(parent()?.title) ?? language.t("command.session.new")) - const childTaskDescription = createMemo(() => { - const id = sessionID() - if (!id) return - return parentMessages() - .flatMap((message) => sync.data.part[message.id] ?? []) - .map((part) => taskDescription(part, id)) - .findLast((value): value is string => !!value) - }) - const childTitle = createMemo(() => { - if (!parentID()) return titleLabel() ?? "" - if (childTaskDescription()) return childTaskDescription() - const value = titleLabel()?.replace(/\s+\(@[^)]+ subagent\)$/, "") - if (value) return value - return language.t("command.session.new") - }) - const showHeader = createMemo(() => !!(titleValue() || parentID())) - const stageCfg = { init: 1, batch: 3 } - const staging = createTimelineStaging({ - sessionKey, - turnStart: () => props.turnStart, - messages: () => props.renderedUserMessages, - config: stageCfg, - }) - - const [title, setTitle] = createStore({ - draft: "", - editing: false, - menuOpen: false, - pendingRename: false, - pendingShare: false, - }) - let titleRef: HTMLInputElement | undefined - - const [share, setShare] = createStore({ - open: false, - dismiss: null as "escape" | "outside" | null, - }) - const [bar, setBar] = createStore({ - ms: pace(640), - }) - - let more: HTMLButtonElement | undefined - let head: HTMLDivElement | undefined - - createResizeObserver( - () => head, - () => { - if (!head || head.clientWidth <= 0) return - setBar("ms", pace(head.clientWidth)) - }, - ) - - const viewShare = () => { - const url = shareUrl() - if (!url) return - platform.openLink(url) - } - - const errorMessage = (err: unknown) => { - if (err && typeof err === "object" && "data" in err) { - const data = (err as { data?: { message?: string } }).data - if (data?.message) return data.message - } - if (err instanceof Error) return err.message - return language.t("common.requestFailed") - } - - const shareMutation = useMutation(() => ({ - mutationFn: (id: string) => globalSDK.client.session.share({ sessionID: id, directory: sdk.directory }), - onError: (err) => { - console.error("Failed to share session", err) - }, - })) - - const unshareMutation = useMutation(() => ({ - mutationFn: (id: string) => globalSDK.client.session.unshare({ sessionID: id, directory: sdk.directory }), - onError: (err) => { - console.error("Failed to unshare session", err) - }, - })) - - const titleMutation = useMutation(() => ({ - mutationFn: (input: { id: string; title: string }) => - sdk.client.session.update({ sessionID: input.id, title: input.title }), - onSuccess: (_, input) => { - sync.set( - produce((draft) => { - const index = draft.session.findIndex((s) => s.id === input.id) - if (index !== -1) draft.session[index].title = input.title - }), - ) - setTitle("editing", false) - }, - onError: (err) => { - showToast({ - title: language.t("common.requestFailed"), - description: errorMessage(err), - }) - }, - })) - - const shareSession = () => { - const id = sessionID() - if (!id || shareMutation.isPending) return - if (!shareEnabled()) return - shareMutation.mutate(id) - } - - const unshareSession = () => { - const id = sessionID() - if (!id || unshareMutation.isPending) return - if (!shareEnabled()) return - unshareMutation.mutate(id) - } - - createEffect( - on( - sessionKey, - () => - setTitle({ - draft: "", - editing: false, - menuOpen: false, - pendingRename: false, - pendingShare: false, - }), - { defer: true }, - ), - ) - - createEffect( - on( - () => [parentID(), childTaskDescription()] as const, - ([id, description]) => { - if (!id || description) return - if (sync.data.message[id] !== undefined) return - void sync.session.sync(id) - }, - { defer: true }, - ), - ) - - const openTitleEditor = () => { - if (!sessionID() || parentID()) return - setTitle({ editing: true, draft: titleLabel() ?? "" }) - requestAnimationFrame(() => { - titleRef?.focus() - titleRef?.select() - }) - } - - const closeTitleEditor = () => { - if (titleMutation.isPending) return - setTitle("editing", false) - } - - const saveTitleEditor = () => { - const id = sessionID() - if (!id) return - if (titleMutation.isPending) return - - const next = title.draft.trim() - if (!next || next === (titleLabel() ?? "")) { - setTitle("editing", false) - return - } - - titleMutation.mutate({ id, title: next }) - } - - const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => { - if (params.id !== sessionID) return - if (parentID) { - navigate(`/${params.dir}/session/${parentID}`) - return - } - if (nextSessionID) { - navigate(`/${params.dir}/session/${nextSessionID}`) - return - } - navigate(`/${params.dir}/session`) - } - - const archiveSession = async (sessionID: string) => { - const session = sync.session.get(sessionID) - if (!session) return - - const sessions = sync.data.session ?? [] - const index = sessions.findIndex((s) => s.id === sessionID) - const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) - - await sdk.client.session - .update({ sessionID, time: { archived: Date.now() } }) - .then(() => { - sync.set( - produce((draft) => { - const index = draft.session.findIndex((s) => s.id === sessionID) - if (index !== -1) draft.session.splice(index, 1) - }), - ) - navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) - }) - .catch((err) => { - showToast({ - title: language.t("common.requestFailed"), - description: errorMessage(err), - }) - }) - } - - const deleteSession = async (sessionID: string) => { - const session = sync.session.get(sessionID) - if (!session) return false - - const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived) - const index = sessions.findIndex((s) => s.id === sessionID) - const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) - - const result = await sdk.client.session - .delete({ sessionID }) - .then((x) => x.data) - .catch((err) => { - showToast({ - title: language.t("session.delete.failed.title"), - description: errorMessage(err), - }) - return false - }) - - if (!result) return false - - sync.set( - produce((draft) => { - const removed = new Set([sessionID]) - - const byParent = new Map() - for (const item of draft.session) { - const parentID = item.parentID - if (!parentID) continue - const existing = byParent.get(parentID) - if (existing) { - existing.push(item.id) - continue - } - byParent.set(parentID, [item.id]) - } - - const stack = [sessionID] - while (stack.length) { - const parentID = stack.pop() - if (!parentID) continue - - const children = byParent.get(parentID) - if (!children) continue - - for (const child of children) { - if (removed.has(child)) continue - removed.add(child) - stack.push(child) - } - } - - draft.session = draft.session.filter((s) => !removed.has(s.id)) - }), - ) - - navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) - return true - } - - const navigateParent = () => { - const id = parentID() - if (!id) return - navigate(`/${params.dir}/session/${id}`) - } - - function DialogDeleteSession(props: { sessionID: string }) { - const name = createMemo( - () => sessionTitle(sync.session.get(props.sessionID)?.title) ?? language.t("command.session.new"), - ) - const handleDelete = async () => { - await deleteSession(props.sessionID) - dialog.close() - } - - return ( - -
-
- - {language.t("session.delete.confirm", { name: name() })} - -
-
- - -
-
-
- ) - } - - return ( - {props.mobileFallback}
} - > -
-
- -
- { - const root = e.currentTarget - const delta = normalizeWheelDelta({ - deltaY: e.deltaY, - deltaMode: e.deltaMode, - rootHeight: root.clientHeight, - }) - if (!delta) return - markBoundaryGesture({ root, target: e.target, delta, onMarkScrollGesture: props.onMarkScrollGesture }) - }} - onTouchStart={(e) => { - touchGesture = e.touches[0]?.clientY - }} - onTouchMove={(e) => { - const next = e.touches[0]?.clientY - const prev = touchGesture - touchGesture = next - if (next === undefined || prev === undefined) return - - const delta = prev - next - if (!delta) return - - const root = e.currentTarget - markBoundaryGesture({ root, target: e.target, delta, onMarkScrollGesture: props.onMarkScrollGesture }) - }} - onTouchEnd={() => { - touchGesture = undefined - }} - onTouchCancel={() => { - touchGesture = undefined - }} - onPointerDown={(e) => { - if (e.target !== e.currentTarget) return - props.onMarkScrollGesture(e.currentTarget) - }} - onScroll={(e) => { - props.onScheduleScrollState(e.currentTarget) - props.onTurnBackfillScroll() - if (!props.hasScrollGesture()) return - props.onUserScroll() - props.onAutoScrollHandleScroll() - props.onMarkScrollGesture(e.currentTarget) - }} - onClick={props.onAutoScrollInteraction} - class="relative min-w-0 w-full h-full" - style={{ - "--session-title-height": showHeader() ? "40px" : "0px", - "--sticky-accordion-top": showHeader() ? "48px" : "0px", - }} - > -
- -
{ - head = el - setBar("ms", pace(el.clientWidth)) - }} - data-session-title - classList={{ - "sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true, - relative: true, - "w-full": true, - "pb-4": true, - "pl-2 pr-3 md:pl-4 md:pr-3": true, - "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered, - }} - > - - - -
- 0 || props.historyMore}> -
- -
-
- - {(messageID) => { - const active = createMemo(() => activeMessageID() === messageID) - const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], { - equals: (a, b) => - a.length === b.length && - a.every( - (c, i) => - c.path === b[i].path && - c.comment === b[i].comment && - c.selection?.startLine === b[i].selection?.startLine && - c.selection?.endLine === b[i].selection?.endLine, - ), - }) - const commentCount = createMemo(() => comments().length) - return ( -
- 0}> -
-
-
- - {(commentAccessor: () => MessageComment) => { - const comment = createMemo(() => commentAccessor()) - return ( - - {(c) => ( -
-
- - {getFilename(c().path)} - - {(selection) => ( - - {selection().startLine === selection().endLine - ? `:${selection().startLine}` - : `:${selection().startLine}-${selection().endLine}`} - - )} - -
-
- {c().comment} -
-
- )} -
- ) - }} -
-
-
-
-
- -
- ) - }} -
-
-
- -
- - ) -} diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx deleted file mode 100644 index 47cfb330cbd..00000000000 --- a/packages/app/src/pages/session/review-tab.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { createEffect, onCleanup, type JSX } from "solid-js" -import { makeEventListener } from "@solid-primitives/event-listener" -import type { SnapshotFileDiff, VcsFileDiff } from "@kilocode/sdk/v2" -import { SessionReview } from "@opencode-ai/ui/session-review" -import type { - SessionReviewCommentActions, - SessionReviewCommentDelete, - SessionReviewCommentUpdate, -} from "@opencode-ai/ui/session-review" -import type { SelectedLineRange } from "@/context/file" -import { useSDK } from "@/context/sdk" -import { useLayout } from "@/context/layout" -import type { LineComment } from "@/context/comments" - -export type DiffStyle = "unified" | "split" - -type ReviewDiff = SnapshotFileDiff | VcsFileDiff - -export interface SessionReviewTabProps { - title?: JSX.Element - empty?: JSX.Element - diffs: () => ReviewDiff[] - view: () => ReturnType["view"]> - diffStyle: DiffStyle - onDiffStyleChange?: (style: DiffStyle) => void - onViewFile?: (file: string) => void - onLineComment?: (comment: { file: string; selection: SelectedLineRange; comment: string; preview?: string }) => void - onLineCommentUpdate?: (comment: SessionReviewCommentUpdate) => void - onLineCommentDelete?: (comment: SessionReviewCommentDelete) => void - lineCommentActions?: SessionReviewCommentActions - comments?: LineComment[] - focusedComment?: { file: string; id: string } | null - onFocusedCommentChange?: (focus: { file: string; id: string } | null) => void - focusedFile?: string - onScrollRef?: (el: HTMLDivElement) => void - commentMentions?: { - items: (query: string) => string[] | Promise - } - classes?: { - root?: string - header?: string - container?: string - } -} - -export function SessionReviewTab(props: SessionReviewTabProps) { - let scroll: HTMLDivElement | undefined - let restoreFrame: number | undefined - let userInteracted = false - let restored: { x: number; y: number } | undefined - - const sdk = useSDK() - const layout = useLayout() - - const readFile = async (path: string) => { - return sdk.client.file - .read({ path }) - .then((x) => x.data) - .catch((error) => { - console.debug("[session-review] failed to read file", { path, error }) - return undefined - }) - } - - const handleInteraction = () => { - userInteracted = true - - if (restoreFrame !== undefined) { - cancelAnimationFrame(restoreFrame) - restoreFrame = undefined - } - } - - const doRestore = () => { - restoreFrame = undefined - const el = scroll - if (!el || !layout.ready() || userInteracted) return - if (el.clientHeight === 0 || el.clientWidth === 0) return - - const s = props.view().scroll("review") - if (!s || (s.x === 0 && s.y === 0)) return - - const maxY = Math.max(0, el.scrollHeight - el.clientHeight) - const maxX = Math.max(0, el.scrollWidth - el.clientWidth) - - const targetY = Math.min(s.y, maxY) - const targetX = Math.min(s.x, maxX) - - if (el.scrollTop === targetY && el.scrollLeft === targetX) return - - if (el.scrollTop !== targetY) el.scrollTop = targetY - if (el.scrollLeft !== targetX) el.scrollLeft = targetX - restored = { x: el.scrollLeft, y: el.scrollTop } - } - - const queueRestore = () => { - if (userInteracted || restoreFrame !== undefined) return - restoreFrame = requestAnimationFrame(doRestore) - } - - const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => { - const el = event.currentTarget - const prev = restored - if (prev && el.scrollTop === prev.y && el.scrollLeft === prev.x) { - restored = undefined - return - } - - restored = undefined - handleInteraction() - if (!layout.ready()) return - if (el.clientHeight === 0 || el.clientWidth === 0) return - - props.view().setScroll("review", { - x: el.scrollLeft, - y: el.scrollTop, - }) - } - - createEffect(() => { - props.diffs().length - props.diffStyle - if (!layout.ready()) return - queueRestore() - }) - - onCleanup(() => { - if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame) - }) - - return ( - { - scroll = el - makeEventListener(el, "wheel", handleInteraction, { passive: true, capture: true }) - makeEventListener(el, "mousewheel", handleInteraction, { passive: true, capture: true }) - makeEventListener(el, "pointerdown", handleInteraction, { passive: true, capture: true }) - makeEventListener(el, "touchstart", handleInteraction, { passive: true, capture: true }) - makeEventListener(el, "keydown", handleInteraction, { capture: true }) - props.onScrollRef?.(el) - queueRestore() - }} - onScroll={handleScroll} - onDiffRendered={queueRestore} - open={props.view().review.open()} - onOpenChange={props.view().review.setOpen} - classes={{ - root: props.classes?.root ?? "pr-3", - header: props.classes?.header ?? "px-3", - container: props.classes?.container ?? "pl-3", - }} - diffs={props.diffs()} - diffStyle={props.diffStyle} - onDiffStyleChange={props.onDiffStyleChange} - onViewFile={props.onViewFile} - focusedFile={props.focusedFile} - readFile={readFile} - onLineComment={props.onLineComment} - onLineCommentUpdate={props.onLineCommentUpdate} - onLineCommentDelete={props.onLineCommentDelete} - lineCommentActions={props.lineCommentActions} - lineCommentMention={props.commentMentions} - comments={props.comments} - focusedComment={props.focusedComment} - onFocusedCommentChange={props.onFocusedCommentChange} - /> - ) -} diff --git a/packages/app/src/pages/session/session-layout.ts b/packages/app/src/pages/session/session-layout.ts deleted file mode 100644 index 113411150da..00000000000 --- a/packages/app/src/pages/session/session-layout.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useParams } from "@solidjs/router" -import { createMemo } from "solid-js" -import { useLayout } from "@/context/layout" - -export const useSessionKey = () => { - const params = useParams() - const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) - return { params, sessionKey } -} - -export const useSessionLayout = () => { - const layout = useLayout() - const { params, sessionKey } = useSessionKey() - return { - params, - sessionKey, - tabs: createMemo(() => layout.tabs(sessionKey)), - view: createMemo(() => layout.view(sessionKey)), - } -} diff --git a/packages/app/src/pages/session/session-model-helpers.test.ts b/packages/app/src/pages/session/session-model-helpers.test.ts deleted file mode 100644 index 36f0f0772cd..00000000000 --- a/packages/app/src/pages/session/session-model-helpers.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { UserMessage } from "@kilocode/sdk/v2" -import { resetSessionModel, syncSessionModel } from "./session-model-helpers" - -const message = (input?: { agent?: string; model?: UserMessage["model"] }) => - ({ - id: "msg", - sessionID: "session", - role: "user", - time: { created: 1 }, - agent: input?.agent ?? "build", - model: input?.model ?? { providerID: "anthropic", modelID: "claude-sonnet-4" }, - }) as UserMessage - -describe("syncSessionModel", () => { - test("restores the last message through session state", () => { - const calls: unknown[] = [] - - syncSessionModel( - { - session: { - restore(value) { - calls.push(value) - }, - reset() {}, - }, - }, - message({ model: { providerID: "anthropic", modelID: "claude-sonnet-4", variant: "high" } }), - ) - - expect(calls).toEqual([ - message({ model: { providerID: "anthropic", modelID: "claude-sonnet-4", variant: "high" } }), - ]) - }) -}) - -describe("resetSessionModel", () => { - test("clears draft session state", () => { - const calls: string[] = [] - - resetSessionModel({ - session: { - reset() { - calls.push("reset") - }, - restore() {}, - }, - }) - - expect(calls).toEqual(["reset"]) - }) -}) diff --git a/packages/app/src/pages/session/session-model-helpers.ts b/packages/app/src/pages/session/session-model-helpers.ts deleted file mode 100644 index 8536f2e9eeb..00000000000 --- a/packages/app/src/pages/session/session-model-helpers.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { UserMessage } from "@kilocode/sdk/v2" - -type Local = { - session: { - reset(): void - restore(msg: UserMessage): void - } -} - -export const resetSessionModel = (local: Local) => { - local.session.reset() -} - -export const syncSessionModel = (local: Local, msg: UserMessage) => { - local.session.restore(msg) -} diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx deleted file mode 100644 index 77d30ba3a82..00000000000 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ /dev/null @@ -1,453 +0,0 @@ -import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js" -import { createStore } from "solid-js/store" -import { createMediaQuery } from "@solid-primitives/media" -import { Tabs } from "@opencode-ai/ui/tabs" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { ResizeHandle } from "@opencode-ai/ui/resize-handle" -import { Mark } from "@opencode-ai/ui/logo" -import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" -import type { DragEvent } from "@thisbeyond/solid-dnd" -import type { SnapshotFileDiff, VcsFileDiff } from "@kilocode/sdk/v2" -import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" -import { useDialog } from "@opencode-ai/ui/context/dialog" - -import FileTree from "@/components/file-tree" -import { SessionContextUsage } from "@/components/session-context-usage" -import { SessionContextTab, SortableTab, FileVisual } from "@/components/session" -import { useCommand } from "@/context/command" -import { useFile, type SelectedLineRange } from "@/context/file" -import { useLanguage } from "@/context/language" -import { useLayout } from "@/context/layout" -import { usePlatform } from "@/context/platform" -import { useSettings } from "@/context/settings" -import { useSync } from "@/context/sync" -import { createFileTabListSync } from "@/pages/session/file-tab-scroll" -import { FileTabContent } from "@/pages/session/file-tabs" -import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers" -import { setSessionHandoff } from "@/pages/session/handoff" -import { useSessionLayout } from "@/pages/session/session-layout" - -export function SessionSidePanel(props: { - canReview: () => boolean - diffs: () => (SnapshotFileDiff | VcsFileDiff)[] - diffsReady: () => boolean - empty: () => string - hasReview: () => boolean - reviewCount: () => number - reviewPanel: () => JSX.Element - activeDiff?: string - focusReviewDiff: (path: string) => void - reviewSnap: boolean - size: Sizing -}) { - const layout = useLayout() - const platform = usePlatform() - const settings = useSettings() - const sync = useSync() - const file = useFile() - const language = useLanguage() - const command = useCommand() - const dialog = useDialog() - const { sessionKey, tabs, view } = useSessionLayout() - - const isDesktop = createMediaQuery("(min-width: 768px)") - const shown = createMemo( - () => - platform.platform !== "desktop" || - import.meta.env.VITE_KILO_CHANNEL !== "beta" || - settings.general.showFileTree(), - ) - - const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) - const fileOpen = createMemo(() => isDesktop() && shown() && layout.fileTree.opened()) - const open = createMemo(() => reviewOpen() || fileOpen()) - const reviewTab = createMemo(() => isDesktop()) - const panelWidth = createMemo(() => { - if (!open()) return "0px" - if (reviewOpen()) return `calc(100% - ${layout.session.width()}px)` - return `${layout.fileTree.width()}px` - }) - const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px")) - - const diffFiles = createMemo(() => props.diffs().map((d) => d.file)) - const kinds = createMemo(() => { - const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => { - if (!a) return b - if (a === b) return a - return "mix" as const - } - - const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "") - - const out = new Map() - for (const diff of props.diffs()) { - const file = normalize(diff.file) - const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix" - - out.set(file, kind) - - const parts = file.split("/") - for (const [idx] of parts.slice(0, -1).entries()) { - const dir = parts.slice(0, idx + 1).join("/") - if (!dir) continue - out.set(dir, merge(out.get(dir), kind)) - } - } - return out - }) - - const empty = (msg: string) => ( -
-
-
-
{msg}
-
-
- ) - - const nofiles = createMemo(() => { - const state = file.tree.state("") - if (!state?.loaded) return false - return file.tree.children("").length === 0 - }) - - const normalizeTab = (tab: string) => { - if (!tab.startsWith("file://")) return tab - return file.tab(tab) - } - - const openReviewPanel = () => { - if (!view().reviewPanel.opened()) view().reviewPanel.open() - } - - const openTab = createOpenSessionFileTab({ - normalizeTab, - openTab: tabs().open, - pathFromTab: file.pathFromTab, - loadFile: file.load, - openReviewPanel, - setActive: tabs().setActive, - }) - - const tabState = createSessionTabs({ - tabs, - pathFromTab: file.pathFromTab, - normalizeTab, - review: reviewTab, - hasReview: props.canReview, - }) - const contextOpen = tabState.contextOpen - const openedTabs = tabState.openedTabs - const activeTab = tabState.activeTab - const activeFileTab = tabState.activeFileTab - - const fileTreeTab = () => layout.fileTree.tab() - - const setFileTreeTabValue = (value: string) => { - if (value !== "changes" && value !== "all") return - layout.fileTree.setTab(value) - } - - const showAllFiles = () => { - if (fileTreeTab() !== "changes") return - layout.fileTree.setTab("all") - } - - const [store, setStore] = createStore({ - activeDraggable: undefined as string | undefined, - }) - - const handleDragStart = (event: unknown) => { - const id = getDraggableId(event) - if (!id) return - setStore("activeDraggable", id) - } - - const handleDragOver = (event: DragEvent) => { - const { draggable, droppable } = event - if (!draggable || !droppable) return - - const currentTabs = tabs().all() - const toIndex = getTabReorderIndex(currentTabs, draggable.id.toString(), droppable.id.toString()) - if (toIndex === undefined) return - tabs().move(draggable.id.toString(), toIndex) - } - - const handleDragEnd = () => { - setStore("activeDraggable", undefined) - } - - createEffect(() => { - if (!file.ready()) return - - setSessionHandoff(sessionKey(), { - files: tabs() - .all() - .reduce>((acc, tab) => { - const path = file.pathFromTab(tab) - if (!path) return acc - - const selected = file.selectedLines(path) - acc[path] = - selected && typeof selected === "object" && "start" in selected && "end" in selected - ? (selected as SelectedLineRange) - : null - - return acc - }, {}), - }) - }) - - return ( - -
- - - ) -} diff --git a/packages/app/src/pages/session/terminal-label.ts b/packages/app/src/pages/session/terminal-label.ts deleted file mode 100644 index 8a34712e436..00000000000 --- a/packages/app/src/pages/session/terminal-label.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { isDefaultTitle as isDefaultTerminalTitle } from "@/context/terminal-title" - -export const terminalTabLabel = (input: { - title?: string - titleNumber?: number - t: (key: string, vars?: Record) => string -}) => { - const title = input.title ?? "" - const number = input.titleNumber ?? 0 - const isDefaultTitle = Number.isFinite(number) && number > 0 && isDefaultTerminalTitle(title, number) - - if (title && !isDefaultTitle) return title - if (number > 0) return input.t("terminal.title.numbered", { number }) - if (title) return title - return input.t("terminal.title") -} diff --git a/packages/app/src/pages/session/terminal-panel.test.ts b/packages/app/src/pages/session/terminal-panel.test.ts deleted file mode 100644 index 43eeec32f21..00000000000 --- a/packages/app/src/pages/session/terminal-panel.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { terminalTabLabel } from "./terminal-label" - -const t = (key: string, vars?: Record) => { - if (key === "terminal.title.numbered") return `Terminal ${vars?.number}` - if (key === "terminal.title") return "Terminal" - return key -} - -describe("terminalTabLabel", () => { - test("returns custom title unchanged", () => { - const label = terminalTabLabel({ title: "server", titleNumber: 3, t }) - expect(label).toBe("server") - }) - - test("normalizes default numbered title", () => { - const label = terminalTabLabel({ title: "Terminal 2", titleNumber: 2, t }) - expect(label).toBe("Terminal 2") - }) - - test("falls back to generic title", () => { - const label = terminalTabLabel({ title: "", titleNumber: 0, t }) - expect(label).toBe("Terminal") - }) -}) diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx deleted file mode 100644 index 2c2d9817f0c..00000000000 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import { For, Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js" -import { createStore } from "solid-js/store" -import { makeEventListener } from "@solid-primitives/event-listener" -import { Tabs } from "@opencode-ai/ui/tabs" -import { ResizeHandle } from "@opencode-ai/ui/resize-handle" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" -import type { DragEvent } from "@thisbeyond/solid-dnd" -import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" - -import { SortableTerminalTab } from "@/components/session" -import { Terminal } from "@/components/terminal" -import { useCommand } from "@/context/command" -import { useLanguage } from "@/context/language" -import { useLayout } from "@/context/layout" -import { useTerminal } from "@/context/terminal" -import { terminalTabLabel } from "@/pages/session/terminal-label" -import { createSizing, focusTerminalById } from "@/pages/session/helpers" -import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff" -import { useSessionLayout } from "@/pages/session/session-layout" - -export function TerminalPanel() { - const delays = [120, 240] - const layout = useLayout() - const terminal = useTerminal() - const language = useLanguage() - const command = useCommand() - const { params, view } = useSessionLayout() - - const opened = createMemo(() => view().terminal.opened()) - const size = createSizing() - const height = createMemo(() => layout.terminal.height()) - const close = () => view().terminal.close() - let root: HTMLDivElement | undefined - - const [store, setStore] = createStore({ - autoCreated: false, - activeDraggable: undefined as string | undefined, - view: typeof window === "undefined" ? 1000 : (window.visualViewport?.height ?? window.innerHeight), - }) - - const max = () => store.view * 0.6 - const pane = () => Math.min(height(), max()) - - onMount(() => { - if (typeof window === "undefined") return - - const sync = () => setStore("view", window.visualViewport?.height ?? window.innerHeight) - const port = window.visualViewport - - sync() - makeEventListener(window, "resize", sync) - if (port) makeEventListener(port, "resize", sync) - }) - - createEffect(() => { - if (!opened()) { - setStore("autoCreated", false) - return - } - - if (!terminal.ready() || terminal.all().length !== 0 || store.autoCreated) return - terminal.new() - setStore("autoCreated", true) - }) - - createEffect( - on( - () => terminal.all().length, - (count, prevCount) => { - if (prevCount === undefined || prevCount <= 0 || count !== 0) return - if (!opened()) return - close() - }, - ), - ) - - const focus = (id: string) => { - focusTerminalById(id) - - const frame = requestAnimationFrame(() => { - if (!opened()) return - if (terminal.active() !== id) return - focusTerminalById(id) - }) - - const timers = delays.map((ms) => - window.setTimeout(() => { - if (!opened()) return - if (terminal.active() !== id) return - focusTerminalById(id) - }, ms), - ) - - return () => { - cancelAnimationFrame(frame) - for (const timer of timers) clearTimeout(timer) - } - } - - createEffect( - on( - () => [opened(), terminal.active()] as const, - ([next, id]) => { - if (!next || !id) return - const stop = focus(id) - onCleanup(stop) - }, - ), - ) - - createEffect(() => { - if (opened()) return - const active = document.activeElement - if (!(active instanceof HTMLElement)) return - if (!root?.contains(active)) return - active.blur() - }) - - createEffect(() => { - const dir = params.dir - if (!dir) return - if (!terminal.ready()) return - language.locale() - - setTerminalHandoff( - dir, - terminal.all().map((pty) => - terminalTabLabel({ - title: pty.title, - titleNumber: pty.titleNumber, - t: language.t as (key: string, vars?: Record) => string, - }), - ), - ) - }) - - const handoff = createMemo(() => { - const dir = params.dir - if (!dir) return [] - return getTerminalHandoff(dir) ?? [] - }) - - const all = terminal.all - const ids = createMemo(() => all().map((pty) => pty.id)) - - const handleTerminalDragStart = (event: unknown) => { - const id = getDraggableId(event) - if (!id) return - setStore("activeDraggable", id) - } - - const handleTerminalDragOver = (event: DragEvent) => { - const { draggable, droppable } = event - if (!draggable || !droppable) return - - const terminals = terminal.all() - const fromIndex = terminals.findIndex((t) => t.id === draggable.id.toString()) - const toIndex = terminals.findIndex((t) => t.id === droppable.id.toString()) - if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) { - terminal.move(draggable.id.toString(), toIndex) - } - } - - const handleTerminalDragEnd = () => { - setStore("activeDraggable", undefined) - - const activeId = terminal.active() - if (!activeId) return - requestAnimationFrame(() => { - if (terminal.active() !== activeId) return - focusTerminalById(activeId) - }) - } - - return ( -
-
- - -
- - {(title) => ( -
- {title} -
- )} -
-
-
- {language.t("common.loading")} - {language.t("common.loading.ellipsis")} -
-
-
{language.t("terminal.loading")}
-
- } - > - - - -
- terminal.open(id)} - class="!h-auto !flex-none" - > - - - {(pty) => } - -
- - - -
-
-
-
- - {(id) => { - const ops = terminal.bind() - return ( - pty.id === id)}> - {(pty) => ( -
- ops.trim(id)} - onCleanup={ops.update} - onConnectError={() => ops.clone(id)} - /> -
- )} -
- ) - }} -
-
-
- - - {(id) => ( - pty.id === id)}> - {(t) => ( -
- {terminalTabLabel({ - title: t().title, - titleNumber: t().titleNumber, - t: language.t as (key: string, vars?: Record) => string, - })} -
- )} -
- )} -
-
-
-
-
-
- ) -} diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx deleted file mode 100644 index 37556067231..00000000000 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ /dev/null @@ -1,585 +0,0 @@ -import { useNavigate } from "@solidjs/router" -import { useCommand, type CommandOption } from "@/context/command" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" -import { useFile, selectionFromLines, type FileSelection, type SelectedLineRange } from "@/context/file" -import { useLanguage } from "@/context/language" -import { useLayout } from "@/context/layout" -import { useLocal } from "@/context/local" -import { usePermission } from "@/context/permission" -import { usePlatform } from "@/context/platform" -import { usePrompt } from "@/context/prompt" -import { useSDK } from "@/context/sdk" -import { useSettings } from "@/context/settings" -import { useSync } from "@/context/sync" -import { useTerminal } from "@/context/terminal" -import { showToast } from "@opencode-ai/ui/toast" -import { findLast } from "@opencode-ai/shared/util/array" -import { createSessionTabs } from "@/pages/session/helpers" -import { extractPromptFromParts } from "@/utils/prompt" -import { UserMessage } from "@kilocode/sdk/v2" -import { useSessionLayout } from "@/pages/session/session-layout" - -export type SessionCommandContext = { - navigateMessageByOffset: (offset: number) => void - setActiveMessage: (message: UserMessage | undefined) => void - focusInput: () => void - review?: () => boolean -} - -const withCategory = (category: string) => { - return (option: Omit): CommandOption => ({ - ...option, - category, - }) -} - -export const useSessionCommands = (actions: SessionCommandContext) => { - const command = useCommand() - const dialog = useDialog() - const file = useFile() - const language = useLanguage() - const local = useLocal() - const permission = usePermission() - const platform = usePlatform() - const prompt = usePrompt() - const sdk = useSDK() - const settings = useSettings() - const sync = useSync() - const terminal = useTerminal() - const layout = useLayout() - const navigate = useNavigate() - const { params, tabs, view } = useSessionLayout() - - const info = () => { - const id = params.id - if (!id) return - return sync.session.get(id) - } - const hasReview = () => !!params.id - const normalizeTab = (tab: string) => { - if (!tab.startsWith("file://")) return tab - return file.tab(tab) - } - const tabState = createSessionTabs({ - tabs, - pathFromTab: file.pathFromTab, - normalizeTab, - review: actions.review, - hasReview, - }) - const activeFileTab = tabState.activeFileTab - const closableTab = tabState.closableTab - const shown = () => - platform.platform !== "desktop" || import.meta.env.VITE_KILO_CHANNEL !== "beta" || settings.general.showFileTree() - - const idle = { type: "idle" as const } - const status = () => sync.data.session_status[params.id ?? ""] ?? idle - const messages = () => { - const id = params.id - if (!id) return [] - return sync.data.message[id] ?? [] - } - const userMessages = () => messages().filter((m) => m.role === "user") as UserMessage[] - const visibleUserMessages = () => { - const revert = info()?.revert?.messageID - if (!revert) return userMessages() - return userMessages().filter((m) => m.id < revert) - } - - const showAllFiles = () => { - if (layout.fileTree.tab() !== "changes") return - layout.fileTree.setTab("all") - } - - const selectionPreview = (path: string, selection: FileSelection) => { - const content = file.get(path)?.content?.content - if (!content) return undefined - return previewSelectedLines(content, { start: selection.startLine, end: selection.endLine }) - } - - const addSelectionToContext = (path: string, selection: FileSelection) => { - const preview = selectionPreview(path, selection) - prompt.context.add({ type: "file", path, selection, preview }) - } - - const canAddSelectionContext = () => { - const tab = activeFileTab() - if (!tab) return false - const path = file.pathFromTab(tab) - if (!path) return false - return file.selectedLines(path) != null - } - - const navigateMessageByOffset = actions.navigateMessageByOffset - const setActiveMessage = actions.setActiveMessage - const focusInput = actions.focusInput - - const sessionCommand = withCategory(language.t("command.category.session")) - const fileCommand = withCategory(language.t("command.category.file")) - const contextCommand = withCategory(language.t("command.category.context")) - const viewCommand = withCategory(language.t("command.category.view")) - const terminalCommand = withCategory(language.t("command.category.terminal")) - const modelCommand = withCategory(language.t("command.category.model")) - const mcpCommand = withCategory(language.t("command.category.mcp")) - const agentCommand = withCategory(language.t("command.category.agent")) - const permissionsCommand = withCategory(language.t("command.category.permissions")) - - const isAutoAcceptActive = () => { - const sessionID = params.id - if (sessionID) return permission.isAutoAccepting(sessionID, sdk.directory) - return permission.isAutoAcceptingDirectory(sdk.directory) - } - const write = async (value: string) => { - const body = typeof document === "undefined" ? undefined : document.body - if (body) { - const textarea = document.createElement("textarea") - textarea.value = value - textarea.setAttribute("readonly", "") - textarea.style.position = "fixed" - textarea.style.opacity = "0" - textarea.style.pointerEvents = "none" - body.appendChild(textarea) - textarea.select() - const copied = document.execCommand("copy") - body.removeChild(textarea) - if (copied) return true - } - - const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard - if (!clipboard?.writeText) return false - return clipboard.writeText(value).then( - () => true, - () => false, - ) - } - - const copyShare = async (url: string, existing: boolean) => { - if (!(await write(url))) { - showToast({ - title: language.t("toast.session.share.copyFailed.title"), - variant: "error", - }) - return - } - - showToast({ - title: existing ? language.t("session.share.copy.copied") : language.t("toast.session.share.success.title"), - description: language.t("toast.session.share.success.description"), - variant: "success", - }) - } - - const share = async () => { - const sessionID = params.id - if (!sessionID) return - - const existing = info()?.share?.url - if (existing) { - await copyShare(existing, true) - return - } - - const url = await sdk.client.session - .share({ sessionID }) - .then((res) => res.data?.share?.url) - .catch(() => undefined) - if (!url) { - showToast({ - title: language.t("toast.session.share.failed.title"), - description: language.t("toast.session.share.failed.description"), - variant: "error", - }) - return - } - - await copyShare(url, false) - } - - const unshare = async () => { - const sessionID = params.id - if (!sessionID) return - - await sdk.client.session - .unshare({ sessionID }) - .then(() => - showToast({ - title: language.t("toast.session.unshare.success.title"), - description: language.t("toast.session.unshare.success.description"), - variant: "success", - }), - ) - .catch(() => - showToast({ - title: language.t("toast.session.unshare.failed.title"), - description: language.t("toast.session.unshare.failed.description"), - variant: "error", - }), - ) - } - - const openFile = () => { - void import("@/components/dialog-select-file").then((x) => { - dialog.show(() => ) - }) - } - - const closeTab = () => { - const tab = closableTab() - if (!tab) return - tabs().close(tab) - } - - const addSelection = () => { - const tab = activeFileTab() - if (!tab) return - - const path = file.pathFromTab(tab) - if (!path) return - - const range = file.selectedLines(path) as SelectedLineRange | null | undefined - if (!range) { - showToast({ - title: language.t("toast.context.noLineSelection.title"), - description: language.t("toast.context.noLineSelection.description"), - }) - return - } - - addSelectionToContext(path, selectionFromLines(range)) - } - - const openTerminal = () => { - if (terminal.all().length > 0) terminal.new() - view().terminal.open() - } - - const chooseModel = () => { - void import("@/components/dialog-select-model").then((x) => { - dialog.show(() => ) - }) - } - - const chooseMcp = () => { - void import("@/components/dialog-select-mcp").then((x) => { - dialog.show(() => ) - }) - } - - const toggleAutoAccept = () => { - const sessionID = params.id - if (sessionID) permission.toggleAutoAccept(sessionID, sdk.directory) - else permission.toggleAutoAcceptDirectory(sdk.directory) - - const active = sessionID - ? permission.isAutoAccepting(sessionID, sdk.directory) - : permission.isAutoAcceptingDirectory(sdk.directory) - showToast({ - title: active - ? language.t("toast.permissions.autoaccept.on.title") - : language.t("toast.permissions.autoaccept.off.title"), - description: active - ? language.t("toast.permissions.autoaccept.on.description") - : language.t("toast.permissions.autoaccept.off.description"), - }) - } - - const undo = async () => { - const sessionID = params.id - if (!sessionID) return - - if (status().type !== "idle") { - await sdk.client.session.abort({ sessionID }).catch(() => {}) - } - - const revert = info()?.revert?.messageID - const message = findLast(userMessages(), (x) => !revert || x.id < revert) - if (!message) return - - await sdk.client.session.revert({ sessionID, messageID: message.id }) - const parts = sync.data.part[message.id] - if (parts) { - const restored = extractPromptFromParts(parts, { directory: sdk.directory }) - prompt.set(restored) - } - - const prev = findLast(userMessages(), (x) => x.id < message.id) - setActiveMessage(prev) - } - - const redo = async () => { - const sessionID = params.id - if (!sessionID) return - - const revertMessageID = info()?.revert?.messageID - if (!revertMessageID) return - - const next = userMessages().find((x) => x.id > revertMessageID) - if (!next) { - await sdk.client.session.unrevert({ sessionID }) - prompt.reset() - const last = findLast(userMessages(), (x) => x.id >= revertMessageID) - setActiveMessage(last) - return - } - - await sdk.client.session.revert({ sessionID, messageID: next.id }) - const prev = findLast(userMessages(), (x) => x.id < next.id) - setActiveMessage(prev) - } - - const compact = async () => { - const sessionID = params.id - if (!sessionID) return - - const model = local.model.current() - if (!model) { - showToast({ - title: language.t("toast.model.none.title"), - description: language.t("toast.model.none.description"), - }) - return - } - - await sdk.client.session.summarize({ - sessionID, - modelID: model.id, - providerID: model.provider.id, - }) - } - - const fork = () => { - void import("@/components/dialog-fork").then((x) => { - dialog.show(() => ) - }) - } - - const shareCmds = () => { - if (sync.data.config.share === "disabled") return [] - return [ - sessionCommand({ - id: "session.share", - title: info()?.share?.url ? language.t("session.share.copy.copyLink") : language.t("command.session.share"), - description: info()?.share?.url - ? language.t("toast.session.share.success.description") - : language.t("command.session.share.description"), - slash: "share", - disabled: !params.id, - onSelect: share, - }), - sessionCommand({ - id: "session.unshare", - title: language.t("command.session.unshare"), - description: language.t("command.session.unshare.description"), - slash: "unshare", - disabled: !params.id || !info()?.share?.url, - onSelect: unshare, - }), - ] - } - - const sessionCmds = () => [ - sessionCommand({ - id: "session.new", - title: language.t("command.session.new"), - keybind: "mod+shift+s", - slash: "new", - onSelect: () => navigate(`/${params.dir}/session`), - }), - sessionCommand({ - id: "session.undo", - title: language.t("command.session.undo"), - description: language.t("command.session.undo.description"), - slash: "undo", - disabled: !params.id || visibleUserMessages().length === 0, - onSelect: undo, - }), - sessionCommand({ - id: "session.redo", - title: language.t("command.session.redo"), - description: language.t("command.session.redo.description"), - slash: "redo", - disabled: !params.id || !info()?.revert?.messageID, - onSelect: redo, - }), - sessionCommand({ - id: "session.compact", - title: language.t("command.session.compact"), - description: language.t("command.session.compact.description"), - slash: "compact", - disabled: !params.id || visibleUserMessages().length === 0, - onSelect: compact, - }), - sessionCommand({ - id: "session.fork", - title: language.t("command.session.fork"), - description: language.t("command.session.fork.description"), - slash: "fork", - disabled: !params.id || visibleUserMessages().length === 0, - onSelect: fork, - }), - ] - - const fileCmds = () => [ - fileCommand({ - id: "file.open", - title: language.t("command.file.open"), - description: language.t("palette.search.placeholder"), - keybind: "mod+k,mod+p", - slash: "open", - onSelect: openFile, - }), - fileCommand({ - id: "tab.close", - title: language.t("command.tab.close"), - keybind: "mod+w", - disabled: !closableTab(), - onSelect: closeTab, - }), - ] - - const contextCmds = () => [ - contextCommand({ - id: "context.addSelection", - title: language.t("command.context.addSelection"), - description: language.t("command.context.addSelection.description"), - keybind: "mod+shift+l", - disabled: !canAddSelectionContext(), - onSelect: addSelection, - }), - ] - - const viewCmds = () => [ - viewCommand({ - id: "terminal.toggle", - title: language.t("command.terminal.toggle"), - keybind: "ctrl+`", - slash: "terminal", - onSelect: () => view().terminal.toggle(), - }), - viewCommand({ - id: "review.toggle", - title: language.t("command.review.toggle"), - keybind: "mod+shift+r", - onSelect: () => view().reviewPanel.toggle(), - }), - ...(shown() - ? [ - viewCommand({ - id: "fileTree.toggle", - title: language.t("command.fileTree.toggle"), - keybind: "mod+\\", - onSelect: () => layout.fileTree.toggle(), - }), - ] - : []), - viewCommand({ - id: "input.focus", - title: language.t("command.input.focus"), - keybind: "ctrl+l", - onSelect: focusInput, - }), - ] - - const terminalCmds = () => [ - terminalCommand({ - id: "terminal.new", - title: language.t("command.terminal.new"), - description: language.t("command.terminal.new.description"), - keybind: "ctrl+alt+t", - onSelect: openTerminal, - }), - ] - - const messageCmds = () => [ - sessionCommand({ - id: "message.previous", - title: language.t("command.message.previous"), - description: language.t("command.message.previous.description"), - keybind: "mod+alt+[", - disabled: !params.id, - onSelect: () => navigateMessageByOffset(-1), - }), - sessionCommand({ - id: "message.next", - title: language.t("command.message.next"), - description: language.t("command.message.next.description"), - keybind: "mod+alt+]", - disabled: !params.id, - onSelect: () => navigateMessageByOffset(1), - }), - ] - - const modelCmds = () => [ - modelCommand({ - id: "model.choose", - title: language.t("command.model.choose"), - description: language.t("command.model.choose.description"), - keybind: "mod+'", - slash: "model", - onSelect: chooseModel, - }), - modelCommand({ - id: "model.variant.cycle", - title: language.t("command.model.variant.cycle"), - description: language.t("command.model.variant.cycle.description"), - keybind: "shift+mod+d", - onSelect: () => local.model.variant.cycle(), - }), - ] - - const mcpCmds = () => [ - mcpCommand({ - id: "mcp.toggle", - title: language.t("command.mcp.toggle"), - description: language.t("command.mcp.toggle.description"), - keybind: "mod+;", - slash: "mcp", - onSelect: chooseMcp, - }), - ] - - const agentCmds = () => [ - agentCommand({ - id: "agent.cycle", - title: language.t("command.agent.cycle"), - description: language.t("command.agent.cycle.description"), - keybind: "mod+.", - slash: "agent", - onSelect: () => local.agent.move(1), - }), - agentCommand({ - id: "agent.cycle.reverse", - title: language.t("command.agent.cycle.reverse"), - description: language.t("command.agent.cycle.reverse.description"), - keybind: "shift+mod+.", - onSelect: () => local.agent.move(-1), - }), - ] - - const permissionsCmds = () => [ - permissionsCommand({ - id: "permissions.autoaccept", - title: isAutoAcceptActive() - ? language.t("command.permissions.autoaccept.disable") - : language.t("command.permissions.autoaccept.enable"), - keybind: "mod+shift+a", - disabled: false, - onSelect: toggleAutoAccept, - }), - ] - - command.register("session", () => [ - ...sessionCmds(), - ...shareCmds(), - ...fileCmds(), - ...contextCmds(), - ...viewCmds(), - ...terminalCmds(), - ...messageCmds(), - ...modelCmds(), - ...mcpCmds(), - ...agentCmds(), - ...permissionsCmds(), - ]) -} diff --git a/packages/app/src/pages/session/use-session-hash-scroll.test.ts b/packages/app/src/pages/session/use-session-hash-scroll.test.ts deleted file mode 100644 index 7f3389baaac..00000000000 --- a/packages/app/src/pages/session/use-session-hash-scroll.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { messageIdFromHash } from "./message-id-from-hash" - -describe("messageIdFromHash", () => { - test("parses hash with leading #", () => { - expect(messageIdFromHash("#message-abc123")).toBe("abc123") - }) - - test("parses raw hash fragment", () => { - expect(messageIdFromHash("message-42")).toBe("42") - }) - - test("ignores non-message anchors", () => { - expect(messageIdFromHash("#review-panel")).toBeUndefined() - }) -}) diff --git a/packages/app/src/pages/session/use-session-hash-scroll.ts b/packages/app/src/pages/session/use-session-hash-scroll.ts deleted file mode 100644 index 474e2ad3216..00000000000 --- a/packages/app/src/pages/session/use-session-hash-scroll.ts +++ /dev/null @@ -1,215 +0,0 @@ -import type { UserMessage } from "@kilocode/sdk/v2" -import { useLocation, useNavigate } from "@solidjs/router" -import { createEffect, createMemo, onCleanup, onMount } from "solid-js" -import { messageIdFromHash } from "./message-id-from-hash" - -export const useSessionHashScroll = (input: { - sessionKey: () => string - sessionID: () => string | undefined - messagesReady: () => boolean - visibleUserMessages: () => UserMessage[] - historyMore: () => boolean - historyLoading: () => boolean - loadMore: (sessionID: string) => Promise - turnStart: () => number - currentMessageId: () => string | undefined - pendingMessage: () => string | undefined - setPendingMessage: (value: string | undefined) => void - setActiveMessage: (message: UserMessage | undefined) => void - setTurnStart: (value: number) => void - autoScroll: { pause: () => void; forceScrollToBottom: () => void } - scroller: () => HTMLDivElement | undefined - anchor: (id: string) => string - scheduleScrollState: (el: HTMLDivElement) => void - consumePendingMessage: (key: string) => string | undefined -}) => { - const visibleUserMessages = createMemo(() => input.visibleUserMessages()) - const messageById = createMemo(() => new Map(visibleUserMessages().map((m) => [m.id, m]))) - const messageIndex = createMemo(() => new Map(visibleUserMessages().map((m, i) => [m.id, i]))) - let pendingKey = "" - let clearing = false - - const location = useLocation() - const navigate = useNavigate() - - const frames = new Set() - const queue = (fn: () => void) => { - const id = requestAnimationFrame(() => { - frames.delete(id) - fn() - }) - frames.add(id) - } - const cancel = () => { - for (const id of frames) cancelAnimationFrame(id) - frames.clear() - } - - const clearMessageHash = () => { - cancel() - input.consumePendingMessage(input.sessionKey()) - if (input.pendingMessage()) input.setPendingMessage(undefined) - if (!location.hash) return - clearing = true - navigate(location.pathname + location.search, { replace: true }) - } - - const updateHash = (id: string) => { - const hash = `#${input.anchor(id)}` - if (location.hash === hash) return - clearing = false - navigate(location.pathname + location.search + hash, { - replace: true, - }) - } - - const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => { - const root = input.scroller() - if (!root) return false - - const a = el.getBoundingClientRect() - const b = root.getBoundingClientRect() - const sticky = root.querySelector("[data-session-title]") - const inset = sticky instanceof HTMLElement ? sticky.offsetHeight : 0 - const top = Math.max(0, a.top - b.top + root.scrollTop - inset) - root.scrollTo({ top, behavior }) - return true - } - - const seek = (id: string, behavior: ScrollBehavior, left = 4): boolean => { - const el = document.getElementById(input.anchor(id)) - if (el) return scrollToElement(el, behavior) - if (left <= 0) return false - queue(() => { - seek(id, behavior, left - 1) - }) - return false - } - - const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => { - cancel() - if (input.currentMessageId() !== message.id) input.setActiveMessage(message) - - const index = messageIndex().get(message.id) ?? -1 - if (index !== -1 && index < input.turnStart()) { - input.setTurnStart(index) - - queue(() => { - seek(message.id, behavior) - }) - - updateHash(message.id) - return - } - - if (seek(message.id, behavior)) { - updateHash(message.id) - return - } - - updateHash(message.id) - } - - const applyHash = (behavior: ScrollBehavior) => { - const hash = location.hash.slice(1) - if (!hash) { - input.autoScroll.forceScrollToBottom() - const el = input.scroller() - if (el) input.scheduleScrollState(el) - return - } - - const messageId = messageIdFromHash(hash) - if (messageId) { - input.autoScroll.pause() - const msg = messageById().get(messageId) - if (msg) { - scrollToMessage(msg, behavior) - return - } - return - } - - const target = document.getElementById(hash) - if (target) { - input.autoScroll.pause() - scrollToElement(target, behavior) - return - } - - input.autoScroll.forceScrollToBottom() - const el = input.scroller() - if (el) input.scheduleScrollState(el) - } - - createEffect(() => { - const hash = location.hash - if (!hash) clearing = false - if (!input.sessionID() || !input.messagesReady()) return - cancel() - queue(() => applyHash("auto")) - }) - - createEffect(() => { - if (!input.sessionID() || !input.messagesReady()) return - - visibleUserMessages() - input.turnStart() - - let targetId = input.pendingMessage() - if (!targetId) { - const key = input.sessionKey() - if (pendingKey !== key) { - pendingKey = key - const next = input.consumePendingMessage(key) - if (next) { - input.setPendingMessage(next) - targetId = next - } - } - } - - if (!targetId && !clearing) targetId = messageIdFromHash(location.hash) - if (!targetId) return - - const pending = input.pendingMessage() === targetId - const msg = messageById().get(targetId) - if (!msg) return - - if (pending) input.setPendingMessage(undefined) - if (input.currentMessageId() === targetId && !pending) return - - input.autoScroll.pause() - cancel() - queue(() => scrollToMessage(msg, "auto")) - }) - - createEffect(() => { - const sessionID = input.sessionID() - if (!sessionID || !input.messagesReady()) return - - visibleUserMessages() - - let targetId = input.pendingMessage() - if (!targetId && !clearing) targetId = messageIdFromHash(location.hash) - if (!targetId) return - if (messageById().has(targetId)) return - if (!input.historyMore() || input.historyLoading()) return - - void input.loadMore(sessionID) - }) - - onMount(() => { - if (typeof window !== "undefined" && "scrollRestoration" in window.history) { - window.history.scrollRestoration = "manual" - } - }) - - onCleanup(cancel) - - return { - clearMessageHash, - scrollToMessage, - applyHash, - } -} diff --git a/packages/app/src/sst-env.d.ts b/packages/app/src/sst-env.d.ts deleted file mode 100644 index 864dce9dc9a..00000000000 --- a/packages/app/src/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* biome-ignore-all lint: auto-generated */ - -/// -interface ImportMetaEnv {} -interface ImportMeta { - readonly env: ImportMetaEnv -} diff --git a/packages/app/src/theme-preload.test.ts b/packages/app/src/theme-preload.test.ts deleted file mode 100644 index 00d7da23948..00000000000 --- a/packages/app/src/theme-preload.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { beforeEach, describe, expect, test } from "bun:test" - -const src = await Bun.file(new URL("../public/oc-theme-preload.js", import.meta.url)).text() - -const run = () => Function(src)() - -beforeEach(() => { - document.head.innerHTML = "" - document.documentElement.removeAttribute("data-theme") - document.documentElement.removeAttribute("data-color-scheme") - localStorage.clear() - Object.defineProperty(window, "matchMedia", { - value: () => - ({ - matches: false, - }) as MediaQueryList, - configurable: true, - }) -}) - -describe("theme preload", () => { - test("migrates legacy oc-1 to oc-2 before mount", () => { - localStorage.setItem("opencode-theme-id", "oc-1") - localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;") - localStorage.setItem("opencode-theme-css-dark", "--background-base:#000;") - - run() - - expect(document.documentElement.dataset.theme).toBe("oc-2") - expect(document.documentElement.dataset.colorScheme).toBe("light") - expect(localStorage.getItem("opencode-theme-id")).toBe("oc-2") - expect(localStorage.getItem("opencode-theme-css-light")).toBeNull() - expect(localStorage.getItem("opencode-theme-css-dark")).toBeNull() - expect(document.getElementById("oc-theme-preload")).toBeNull() - }) - - test("keeps cached css for non-default themes", () => { - localStorage.setItem("opencode-theme-id", "nightowl") - localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;") - - run() - - expect(document.documentElement.dataset.theme).toBe("nightowl") - expect(document.getElementById("oc-theme-preload")?.textContent).toContain("--background-base:#fff;") - }) -}) diff --git a/packages/app/src/utils/agent.ts b/packages/app/src/utils/agent.ts deleted file mode 100644 index 59da53af102..00000000000 --- a/packages/app/src/utils/agent.ts +++ /dev/null @@ -1,44 +0,0 @@ -const defaults: Record = { - ask: "var(--icon-agent-ask-base)", - build: "var(--icon-agent-build-base)", - docs: "var(--icon-agent-docs-base)", - plan: "var(--icon-agent-plan-base)", -} - -const palette = [ - "var(--icon-agent-ask-base)", - "var(--icon-agent-build-base)", - "var(--icon-agent-docs-base)", - "var(--icon-agent-plan-base)", - "var(--syntax-info)", - "var(--syntax-success)", - "var(--syntax-warning)", - "var(--syntax-property)", - "var(--syntax-constant)", - "var(--text-diff-add-base)", - "var(--text-diff-delete-base)", - "var(--icon-warning-base)", -] - -function tone(name: string) { - let hash = 0 - for (const char of name) hash = (hash * 31 + char.charCodeAt(0)) >>> 0 - return palette[hash % palette.length] -} - -export function agentColor(name: string, custom?: string) { - if (custom) return custom - return defaults[name] ?? defaults[name.toLowerCase()] ?? tone(name.toLowerCase()) -} - -export function messageAgentColor( - list: readonly { role: string; agent?: string }[] | undefined, - agents: readonly { name: string; color?: string }[], -) { - if (!list) return undefined - for (let i = list.length - 1; i >= 0; i--) { - const item = list[i] - if (item.role !== "user" || !item.agent) continue - return agentColor(item.agent, agents.find((agent) => agent.name === item.agent)?.color) - } -} diff --git a/packages/app/src/utils/aim.ts b/packages/app/src/utils/aim.ts deleted file mode 100644 index 23471959e16..00000000000 --- a/packages/app/src/utils/aim.ts +++ /dev/null @@ -1,138 +0,0 @@ -type Point = { x: number; y: number } - -export function createAim(props: { - enabled: () => boolean - active: () => string | undefined - el: () => HTMLElement | undefined - onActivate: (id: string) => void - delay?: number - max?: number - tolerance?: number - edge?: number -}) { - const state = { - locs: [] as Point[], - timer: undefined as number | undefined, - pending: undefined as string | undefined, - over: undefined as string | undefined, - last: undefined as Point | undefined, - } - - const delay = props.delay ?? 250 - const max = props.max ?? 4 - const tolerance = props.tolerance ?? 80 - const edge = props.edge ?? 18 - - const cancel = () => { - if (state.timer !== undefined) clearTimeout(state.timer) - state.timer = undefined - state.pending = undefined - } - - const reset = () => { - cancel() - state.over = undefined - state.last = undefined - state.locs.length = 0 - } - - const move = (event: MouseEvent) => { - if (!props.enabled()) return - const el = props.el() - if (!el) return - - const rect = el.getBoundingClientRect() - const x = event.clientX - const y = event.clientY - if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) return - - state.locs.push({ x, y }) - if (state.locs.length > max) state.locs.shift() - } - - const wait = () => { - if (!props.enabled()) return 0 - if (!props.active()) return 0 - - const el = props.el() - if (!el) return 0 - if (state.locs.length < 2) return 0 - - const rect = el.getBoundingClientRect() - const loc = state.locs[state.locs.length - 1] - if (!loc) return 0 - - const prev = state.locs[0] ?? loc - if (prev.x < rect.left || prev.x > rect.right || prev.y < rect.top || prev.y > rect.bottom) return 0 - if (state.last && loc.x === state.last.x && loc.y === state.last.y) return 0 - - if (rect.right - loc.x <= edge) { - state.last = loc - return delay - } - - const upper = { x: rect.right, y: rect.top - tolerance } - const lower = { x: rect.right, y: rect.bottom + tolerance } - const slope = (a: Point, b: Point) => (b.y - a.y) / (b.x - a.x) - - const decreasing = slope(loc, upper) - const increasing = slope(loc, lower) - const prevDecreasing = slope(prev, upper) - const prevIncreasing = slope(prev, lower) - - if (decreasing < prevDecreasing && increasing > prevIncreasing) { - state.last = loc - return delay - } - - state.last = undefined - return 0 - } - - const activate = (id: string) => { - cancel() - props.onActivate(id) - } - - const request = (id: string) => { - if (!id) return - if (props.active() === id) return - - if (!props.active()) { - activate(id) - return - } - - const ms = wait() - if (ms === 0) { - activate(id) - return - } - - cancel() - state.pending = id - state.timer = window.setTimeout(() => { - state.timer = undefined - if (state.pending !== id) return - state.pending = undefined - if (!props.enabled()) return - if (!props.active()) return - if (state.over !== id) return - props.onActivate(id) - }, ms) - } - - const enter = (id: string, event: MouseEvent) => { - if (!props.enabled()) return - state.over = id - move(event) - request(id) - } - - const leave = (id: string) => { - if (state.over === id) state.over = undefined - if (state.pending === id) cancel() - } - - return { move, enter, leave, activate, request, cancel, reset } -} diff --git a/packages/app/src/utils/base64.ts b/packages/app/src/utils/base64.ts deleted file mode 100644 index f60dff2b6d8..00000000000 --- a/packages/app/src/utils/base64.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { base64Decode } from "@opencode-ai/shared/util/encode" - -export function decode64(value: string | undefined) { - if (value === undefined) return - try { - return base64Decode(value) - } catch { - return - } -} diff --git a/packages/app/src/utils/comment-note.ts b/packages/app/src/utils/comment-note.ts deleted file mode 100644 index 99e87fc81c7..00000000000 --- a/packages/app/src/utils/comment-note.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { FileSelection } from "@/context/file" - -export type PromptComment = { - path: string - selection?: FileSelection - comment: string - preview?: string - origin?: "review" | "file" -} - -function selection(selection: unknown) { - if (!selection || typeof selection !== "object") return undefined - const startLine = Number((selection as FileSelection).startLine) - const startChar = Number((selection as FileSelection).startChar) - const endLine = Number((selection as FileSelection).endLine) - const endChar = Number((selection as FileSelection).endChar) - if (![startLine, startChar, endLine, endChar].every(Number.isFinite)) return undefined - return { - startLine, - startChar, - endLine, - endChar, - } satisfies FileSelection -} - -export function createCommentMetadata(input: PromptComment) { - return { - opencodeComment: { - path: input.path, - selection: input.selection, - comment: input.comment, - preview: input.preview, - origin: input.origin, - }, - } -} - -export function readCommentMetadata(value: unknown) { - if (!value || typeof value !== "object") return - const meta = (value as { opencodeComment?: unknown }).opencodeComment - if (!meta || typeof meta !== "object") return - const path = (meta as { path?: unknown }).path - const comment = (meta as { comment?: unknown }).comment - if (typeof path !== "string" || typeof comment !== "string") return - const preview = (meta as { preview?: unknown }).preview - const origin = (meta as { origin?: unknown }).origin - return { - path, - selection: selection((meta as { selection?: unknown }).selection), - comment, - preview: typeof preview === "string" ? preview : undefined, - origin: origin === "review" || origin === "file" ? origin : undefined, - } satisfies PromptComment -} - -export function formatCommentNote(input: { path: string; selection?: FileSelection; comment: string }) { - const start = input.selection ? Math.min(input.selection.startLine, input.selection.endLine) : undefined - const end = input.selection ? Math.max(input.selection.startLine, input.selection.endLine) : undefined - const range = - start === undefined || end === undefined - ? "this file" - : start === end - ? `line ${start}` - : `lines ${start} through ${end}` - return `The user made the following comment regarding ${range} of ${input.path}: ${input.comment}` -} - -export function parseCommentNote(text: string) { - const match = text.match( - /^The user made the following comment regarding (this file|line (\d+)|lines (\d+) through (\d+)) of (.+?): ([\s\S]+)$/, - ) - if (!match) return - const start = match[2] ? Number(match[2]) : match[3] ? Number(match[3]) : undefined - const end = match[2] ? Number(match[2]) : match[4] ? Number(match[4]) : undefined - return { - path: match[5], - selection: - start !== undefined && end !== undefined - ? { - startLine: start, - startChar: 0, - endLine: end, - endChar: 0, - } - : undefined, - comment: match[6], - } satisfies PromptComment -} diff --git a/packages/app/src/utils/diffs.test.ts b/packages/app/src/utils/diffs.test.ts deleted file mode 100644 index f3f437ae4ff..00000000000 --- a/packages/app/src/utils/diffs.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { SnapshotFileDiff } from "@kilocode/sdk/v2" -import type { Message } from "@kilocode/sdk/v2/client" -import { diffs, message } from "./diffs" - -const item = { - file: "src/app.ts", - patch: "@@ -1 +1 @@\n-old\n+new\n", - additions: 1, - deletions: 1, - status: "modified", -} satisfies SnapshotFileDiff - -describe("diffs", () => { - test("keeps valid arrays", () => { - expect(diffs([item])).toEqual([item]) - }) - - test("wraps a single diff object", () => { - expect(diffs(item)).toEqual([item]) - }) - - test("reads keyed diff objects", () => { - expect(diffs({ a: item })).toEqual([item]) - }) - - test("drops invalid entries", () => { - expect( - diffs([ - item, - { file: "src/bad.ts", additions: 1, deletions: 1 }, - { patch: item.patch, additions: 1, deletions: 1 }, - ]), - ).toEqual([item]) - }) -}) - -describe("message", () => { - test("normalizes user summaries with object diffs", () => { - const input = { - id: "msg_1", - sessionID: "ses_1", - role: "user", - time: { created: 1 }, - agent: "build", - model: { providerID: "openai", modelID: "gpt-5" }, - summary: { - title: "Edit", - diffs: { a: item }, - }, - } as unknown as Message - - expect(message(input)).toMatchObject({ - summary: { - title: "Edit", - diffs: [item], - }, - }) - }) - - test("drops invalid user summaries", () => { - const input = { - id: "msg_1", - sessionID: "ses_1", - role: "user", - time: { created: 1 }, - agent: "build", - model: { providerID: "openai", modelID: "gpt-5" }, - summary: true, - } as unknown as Message - - expect(message(input)).toMatchObject({ summary: undefined }) - }) -}) diff --git a/packages/app/src/utils/diffs.ts b/packages/app/src/utils/diffs.ts deleted file mode 100644 index ccb1c8d3cf9..00000000000 --- a/packages/app/src/utils/diffs.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { SnapshotFileDiff, VcsFileDiff } from "@kilocode/sdk/v2" -import type { Message } from "@kilocode/sdk/v2/client" - -type Diff = SnapshotFileDiff | VcsFileDiff - -function diff(value: unknown): value is Diff { - if (!value || typeof value !== "object" || Array.isArray(value)) return false - if (!("file" in value) || typeof value.file !== "string") return false - if (!("patch" in value) || typeof value.patch !== "string") return false - if (!("additions" in value) || typeof value.additions !== "number") return false - if (!("deletions" in value) || typeof value.deletions !== "number") return false - if (!("status" in value) || value.status === undefined) return true - return value.status === "added" || value.status === "deleted" || value.status === "modified" -} - -function object(value: unknown): value is Record { - return !!value && typeof value === "object" && !Array.isArray(value) -} - -export function diffs(value: unknown): Diff[] { - if (Array.isArray(value) && value.every(diff)) return value - if (Array.isArray(value)) return value.filter(diff) - if (diff(value)) return [value] - if (!object(value)) return [] - return Object.values(value).filter(diff) -} - -export function message(value: Message): Message { - if (value.role !== "user") return value - - const raw = value.summary as unknown - if (raw === undefined) return value - if (!object(raw)) return { ...value, summary: undefined } - - const title = typeof raw.title === "string" ? raw.title : undefined - const body = typeof raw.body === "string" ? raw.body : undefined - const next = diffs(raw.diffs) - - if (title === raw.title && body === raw.body && next === raw.diffs) return value - - return { - ...value, - summary: { - ...(title === undefined ? {} : { title }), - ...(body === undefined ? {} : { body }), - diffs: next, - }, - } -} diff --git a/packages/app/src/utils/id.ts b/packages/app/src/utils/id.ts deleted file mode 100644 index fa27cf4c5f9..00000000000 --- a/packages/app/src/utils/id.ts +++ /dev/null @@ -1,99 +0,0 @@ -import z from "zod" - -const prefixes = { - session: "ses", - message: "msg", - permission: "per", - user: "usr", - part: "prt", - pty: "pty", -} as const - -const LENGTH = 26 -let lastTimestamp = 0 -let counter = 0 - -type Prefix = keyof typeof prefixes -export namespace Identifier { - export function schema(prefix: Prefix) { - return z.string().startsWith(prefixes[prefix]) - } - - export function ascending(prefix: Prefix, given?: string) { - return generateID(prefix, false, given) - } - - export function descending(prefix: Prefix, given?: string) { - return generateID(prefix, true, given) - } -} - -function generateID(prefix: Prefix, descending: boolean, given?: string): string { - if (!given) { - return create(prefix, descending) - } - - if (!given.startsWith(prefixes[prefix])) { - throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`) - } - - return given -} - -function create(prefix: Prefix, descending: boolean, timestamp?: number): string { - const currentTimestamp = timestamp ?? Date.now() - - if (currentTimestamp !== lastTimestamp) { - lastTimestamp = currentTimestamp - counter = 0 - } - - counter += 1 - - let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter) - - if (descending) { - now = ~now - } - - const timeBytes = new Uint8Array(6) - for (let i = 0; i < 6; i += 1) { - timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff)) - } - - return prefixes[prefix] + "_" + bytesToHex(timeBytes) + randomBase62(LENGTH - 12) -} - -function bytesToHex(bytes: Uint8Array): string { - let hex = "" - for (let i = 0; i < bytes.length; i += 1) { - hex += bytes[i].toString(16).padStart(2, "0") - } - return hex -} - -function randomBase62(length: number): string { - const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - const bytes = getRandomBytes(length) - let result = "" - for (let i = 0; i < length; i += 1) { - result += chars[bytes[i] % 62] - } - return result -} - -function getRandomBytes(length: number): Uint8Array { - const bytes = new Uint8Array(length) - const cryptoObj = typeof globalThis !== "undefined" ? globalThis.crypto : undefined - - if (cryptoObj && typeof cryptoObj.getRandomValues === "function") { - cryptoObj.getRandomValues(bytes) - return bytes - } - - for (let i = 0; i < length; i += 1) { - bytes[i] = Math.floor(Math.random() * 256) - } - - return bytes -} diff --git a/packages/app/src/utils/notification-click.test.ts b/packages/app/src/utils/notification-click.test.ts deleted file mode 100644 index fa81b0e0251..00000000000 --- a/packages/app/src/utils/notification-click.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test" -import { handleNotificationClick, setNavigate } from "./notification-click" - -describe("notification click", () => { - afterEach(() => { - setNavigate(undefined as any) - }) - - test("navigates via registered navigate function", () => { - const calls: string[] = [] - setNavigate((href) => calls.push(href)) - handleNotificationClick("/abc/session/123") - expect(calls).toEqual(["/abc/session/123"]) - }) - - test("does not navigate when href is missing", () => { - const calls: string[] = [] - setNavigate((href) => calls.push(href)) - handleNotificationClick(undefined) - expect(calls).toEqual([]) - }) - - test("falls back to location.assign without registered navigate", () => { - handleNotificationClick("/abc/session/123") - // falls back to window.location.assign — no error thrown - }) -}) diff --git a/packages/app/src/utils/notification-click.ts b/packages/app/src/utils/notification-click.ts deleted file mode 100644 index 316b2782062..00000000000 --- a/packages/app/src/utils/notification-click.ts +++ /dev/null @@ -1,13 +0,0 @@ -let nav: ((href: string) => void) | undefined - -export const setNavigate = (fn: (href: string) => void) => { - nav = fn -} - -export const handleNotificationClick = (href?: string) => { - window.focus() - if (!href) return - if (nav) return nav(href) - console.warn("notification-click: navigate function not set, falling back to window.location.assign") - window.location.assign(href) -} diff --git a/packages/app/src/utils/persist.test.ts b/packages/app/src/utils/persist.test.ts deleted file mode 100644 index 673acd224d2..00000000000 --- a/packages/app/src/utils/persist.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test" - -type PersistTestingType = typeof import("./persist").PersistTesting - -class MemoryStorage implements Storage { - private values = new Map() - readonly events: string[] = [] - readonly calls = { get: 0, set: 0, remove: 0 } - - clear() { - this.values.clear() - } - - get length() { - return this.values.size - } - - key(index: number) { - return Array.from(this.values.keys())[index] ?? null - } - - getItem(key: string) { - this.calls.get += 1 - this.events.push(`get:${key}`) - if (key.startsWith("opencode.throw")) throw new Error("storage get failed") - return this.values.get(key) ?? null - } - - setItem(key: string, value: string) { - this.calls.set += 1 - this.events.push(`set:${key}`) - if (key.startsWith("opencode.quota")) throw new DOMException("quota", "QuotaExceededError") - if (key.startsWith("opencode.throw")) throw new Error("storage set failed") - this.values.set(key, value) - } - - removeItem(key: string) { - this.calls.remove += 1 - this.events.push(`remove:${key}`) - if (key.startsWith("opencode.throw")) throw new Error("storage remove failed") - this.values.delete(key) - } -} - -const storage = new MemoryStorage() - -let persistTesting: PersistTestingType - -beforeAll(async () => { - mock.module("@/context/platform", () => ({ - usePlatform: () => ({ platform: "web" }), - })) - - const mod = await import("./persist") - persistTesting = mod.PersistTesting -}) - -beforeEach(() => { - storage.clear() - storage.events.length = 0 - storage.calls.get = 0 - storage.calls.set = 0 - storage.calls.remove = 0 - Object.defineProperty(globalThis, "localStorage", { - value: storage, - configurable: true, - }) -}) - -describe("persist localStorage resilience", () => { - test("does not cache values as persisted when quota write and eviction fail", () => { - const storageApi = persistTesting.localStorageWithPrefix("opencode.quota.scope") - storageApi.setItem("value", '{"value":1}') - - expect(storage.getItem("opencode.quota.scope:value")).toBeNull() - expect(storageApi.getItem("value")).toBeNull() - }) - - test("disables only the failing scope when storage throws", () => { - const bad = persistTesting.localStorageWithPrefix("opencode.throw.scope") - bad.setItem("value", '{"value":1}') - - const before = storage.calls.set - bad.setItem("value", '{"value":2}') - expect(storage.calls.set).toBe(before) - expect(bad.getItem("value")).toBeNull() - - const healthy = persistTesting.localStorageWithPrefix("opencode.safe.scope") - healthy.setItem("value", '{"value":3}') - expect(storage.getItem("opencode.safe.scope:value")).toBe('{"value":3}') - }) - - test("failing fallback scope does not poison direct storage scope", () => { - const broken = persistTesting.localStorageWithPrefix("opencode.throw.scope2") - broken.setItem("value", '{"value":1}') - - const direct = persistTesting.localStorageDirect() - direct.setItem("direct-value", '{"value":5}') - - expect(storage.getItem("direct-value")).toBe('{"value":5}') - }) - - test("normalizer rejects malformed JSON payloads", () => { - const result = persistTesting.normalize({ value: "ok" }, '{"value":"\\x"}') - expect(result).toBeUndefined() - }) - - test("workspace storage sanitizes Windows filename characters", () => { - const result = persistTesting.workspaceStorage("C:\\Users\\foo") - - expect(result).toStartWith("opencode.workspace.") - expect(result.endsWith(".dat")).toBeTrue() - expect(/[:\\/]/.test(result)).toBeFalse() - }) -}) diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts deleted file mode 100644 index 0cac30cb1e2..00000000000 --- a/packages/app/src/utils/persist.ts +++ /dev/null @@ -1,476 +0,0 @@ -import { Platform, usePlatform } from "@/context/platform" -import { makePersisted, type AsyncStorage, type SyncStorage } from "@solid-primitives/storage" -import { checksum } from "@opencode-ai/shared/util/encode" -import { createResource, type Accessor } from "solid-js" -import type { SetStoreFunction, Store } from "solid-js/store" - -type InitType = Promise | string | null -type PersistedWithReady = [ - Store, - SetStoreFunction, - InitType, - Accessor & { promise: undefined | Promise }, -] - -type PersistTarget = { - storage?: string - key: string - legacy?: string[] - migrate?: (value: unknown) => unknown -} - -const LEGACY_STORAGE = "default.dat" -const GLOBAL_STORAGE = "opencode.global.dat" -const LOCAL_PREFIX = "opencode." -const fallback = new Map() - -const CACHE_MAX_ENTRIES = 500 -const CACHE_MAX_BYTES = 8 * 1024 * 1024 - -type CacheEntry = { value: string; bytes: number } -const cache = new Map() -const cacheTotal = { bytes: 0 } - -function cacheDelete(key: string) { - const entry = cache.get(key) - if (!entry) return - cacheTotal.bytes -= entry.bytes - cache.delete(key) -} - -function cachePrune() { - for (;;) { - if (cache.size <= CACHE_MAX_ENTRIES && cacheTotal.bytes <= CACHE_MAX_BYTES) return - const oldest = cache.keys().next().value as string | undefined - if (!oldest) return - cacheDelete(oldest) - } -} - -function cacheSet(key: string, value: string) { - const bytes = value.length * 2 - if (bytes > CACHE_MAX_BYTES) { - cacheDelete(key) - return - } - - const entry = cache.get(key) - if (entry) cacheTotal.bytes -= entry.bytes - cache.delete(key) - cache.set(key, { value, bytes }) - cacheTotal.bytes += bytes - cachePrune() -} - -function cacheGet(key: string) { - const entry = cache.get(key) - if (!entry) return - cache.delete(key) - cache.set(key, entry) - return entry.value -} - -function fallbackDisabled(scope: string) { - return fallback.get(scope) === true -} - -function fallbackSet(scope: string) { - fallback.set(scope, true) -} - -function quota(error: unknown) { - if (error instanceof DOMException) { - if (error.name === "QuotaExceededError") return true - if (error.name === "NS_ERROR_DOM_QUOTA_REACHED") return true - if (error.name === "QUOTA_EXCEEDED_ERR") return true - if (error.code === 22 || error.code === 1014) return true - return false - } - - if (!error || typeof error !== "object") return false - const name = (error as { name?: string }).name - if (name === "QuotaExceededError" || name === "NS_ERROR_DOM_QUOTA_REACHED") return true - if (name && /quota/i.test(name)) return true - - const code = (error as { code?: number }).code - if (code === 22 || code === 1014) return true - - const message = (error as { message?: string }).message - if (typeof message !== "string") return false - if (/quota/i.test(message)) return true - return false -} - -type Evict = { key: string; size: number } - -function evict(storage: Storage, keep: string, value: string) { - const total = storage.length - const indexes = Array.from({ length: total }, (_, index) => index) - const items: Evict[] = [] - - for (const index of indexes) { - const name = storage.key(index) - if (!name) continue - if (!name.startsWith(LOCAL_PREFIX)) continue - if (name === keep) continue - const stored = storage.getItem(name) - items.push({ key: name, size: stored?.length ?? 0 }) - } - - items.sort((a, b) => b.size - a.size) - - for (const item of items) { - storage.removeItem(item.key) - cacheDelete(item.key) - - try { - storage.setItem(keep, value) - cacheSet(keep, value) - return true - } catch (error) { - if (!quota(error)) throw error - } - } - - return false -} - -function write(storage: Storage, key: string, value: string) { - try { - storage.setItem(key, value) - cacheSet(key, value) - return true - } catch (error) { - if (!quota(error)) throw error - } - - try { - storage.removeItem(key) - cacheDelete(key) - storage.setItem(key, value) - cacheSet(key, value) - return true - } catch (error) { - if (!quota(error)) throw error - } - - const ok = evict(storage, key, value) - return ok -} - -function snapshot(value: unknown) { - return JSON.parse(JSON.stringify(value)) as unknown -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value) -} - -function merge(defaults: unknown, value: unknown): unknown { - if (value === undefined) return defaults - if (value === null) return value - - if (Array.isArray(defaults)) { - if (Array.isArray(value)) return value - return defaults - } - - if (isRecord(defaults)) { - if (!isRecord(value)) return defaults - - const result: Record = { ...defaults } - for (const key of Object.keys(value)) { - if (key in defaults) { - result[key] = merge((defaults as Record)[key], (value as Record)[key]) - } else { - result[key] = (value as Record)[key] - } - } - return result - } - - return value -} - -function parse(value: string) { - try { - return JSON.parse(value) as unknown - } catch { - return undefined - } -} - -function normalize(defaults: unknown, raw: string, migrate?: (value: unknown) => unknown) { - const parsed = parse(raw) - if (parsed === undefined) return - const migrated = migrate ? migrate(parsed) : parsed - const merged = merge(defaults, migrated) - return JSON.stringify(merged) -} - -function workspaceStorage(dir: string) { - const head = (dir.slice(0, 12) || "workspace").replace(/[^a-zA-Z0-9._-]/g, "-") - const sum = checksum(dir) ?? "0" - return `opencode.workspace.${head}.${sum}.dat` -} - -function localStorageWithPrefix(prefix: string): SyncStorage { - const base = `${prefix}:` - const scope = `prefix:${prefix}` - const item = (key: string) => base + key - return { - getItem: (key) => { - const name = item(key) - const cached = cacheGet(name) - if (fallbackDisabled(scope)) return cached ?? null - - const stored = (() => { - try { - return localStorage.getItem(name) - } catch { - fallbackSet(scope) - return null - } - })() - if (stored === null) return cached ?? null - cacheSet(name, stored) - return stored - }, - setItem: (key, value) => { - const name = item(key) - if (fallbackDisabled(scope)) return - try { - if (write(localStorage, name, value)) return - } catch { - fallbackSet(scope) - return - } - fallbackSet(scope) - }, - removeItem: (key) => { - const name = item(key) - cacheDelete(name) - if (fallbackDisabled(scope)) return - try { - localStorage.removeItem(name) - } catch { - fallbackSet(scope) - } - }, - } -} - -function localStorageDirect(): SyncStorage { - const scope = "direct" - return { - getItem: (key) => { - const cached = cacheGet(key) - if (fallbackDisabled(scope)) return cached ?? null - - const stored = (() => { - try { - return localStorage.getItem(key) - } catch { - fallbackSet(scope) - return null - } - })() - if (stored === null) return cached ?? null - cacheSet(key, stored) - return stored - }, - setItem: (key, value) => { - if (fallbackDisabled(scope)) return - try { - if (write(localStorage, key, value)) return - } catch { - fallbackSet(scope) - return - } - fallbackSet(scope) - }, - removeItem: (key) => { - cacheDelete(key) - if (fallbackDisabled(scope)) return - try { - localStorage.removeItem(key) - } catch { - fallbackSet(scope) - } - }, - } -} - -export const PersistTesting = { - localStorageDirect, - localStorageWithPrefix, - normalize, - workspaceStorage, -} - -export const Persist = { - global(key: string, legacy?: string[]): PersistTarget { - return { storage: GLOBAL_STORAGE, key, legacy } - }, - workspace(dir: string, key: string, legacy?: string[]): PersistTarget { - return { storage: workspaceStorage(dir), key: `workspace:${key}`, legacy } - }, - session(dir: string, session: string, key: string, legacy?: string[]): PersistTarget { - return { storage: workspaceStorage(dir), key: `session:${session}:${key}`, legacy } - }, - scoped(dir: string, session: string | undefined, key: string, legacy?: string[]): PersistTarget { - if (session) return Persist.session(dir, session, key, legacy) - return Persist.workspace(dir, key, legacy) - }, -} - -export function removePersisted(target: { storage?: string; key: string }, platform?: Platform) { - const isDesktop = platform?.platform === "desktop" && !!platform.storage - - if (isDesktop) { - return platform.storage?.(target.storage)?.removeItem(target.key) - } - - if (!target.storage) { - localStorageDirect().removeItem(target.key) - return - } - - localStorageWithPrefix(target.storage).removeItem(target.key) -} - -export function persisted( - target: string | PersistTarget, - store: [Store, SetStoreFunction], -): PersistedWithReady { - const platform = usePlatform() - const config: PersistTarget = typeof target === "string" ? { key: target } : target - - const defaults = snapshot(store[0]) - const legacy = config.legacy ?? [] - - const isDesktop = platform.platform === "desktop" && !!platform.storage - - const currentStorage = (() => { - if (isDesktop) return platform.storage?.(config.storage) - if (!config.storage) return localStorageDirect() - return localStorageWithPrefix(config.storage) - })() - - const legacyStorage = (() => { - if (!isDesktop) return localStorageDirect() - if (!config.storage) return platform.storage?.() - return platform.storage?.(LEGACY_STORAGE) - })() - - const storage = (() => { - if (!isDesktop) { - const current = currentStorage as SyncStorage - const legacyStore = legacyStorage as SyncStorage - - const api: SyncStorage = { - getItem: (key) => { - const raw = current.getItem(key) - if (raw !== null) { - const next = normalize(defaults, raw, config.migrate) - if (next === undefined) { - current.removeItem(key) - return null - } - if (raw !== next) current.setItem(key, next) - return next - } - - for (const legacyKey of legacy) { - const legacyRaw = legacyStore.getItem(legacyKey) - if (legacyRaw === null) continue - - const next = normalize(defaults, legacyRaw, config.migrate) - if (next === undefined) { - legacyStore.removeItem(legacyKey) - continue - } - current.setItem(key, next) - legacyStore.removeItem(legacyKey) - return next - } - - return null - }, - setItem: (key, value) => { - current.setItem(key, value) - }, - removeItem: (key) => { - current.removeItem(key) - }, - } - - return api - } - - const current = currentStorage as AsyncStorage - const legacyStore = legacyStorage as AsyncStorage | undefined - - const api: AsyncStorage = { - getItem: async (key) => { - const raw = await current.getItem(key) - if (raw !== null) { - const next = normalize(defaults, raw, config.migrate) - if (next === undefined) { - await current.removeItem(key).catch(() => undefined) - return null - } - if (raw !== next) await current.setItem(key, next) - return next - } - - if (!legacyStore) return null - - for (const legacyKey of legacy) { - const legacyRaw = await legacyStore.getItem(legacyKey) - if (legacyRaw === null) continue - - const next = normalize(defaults, legacyRaw, config.migrate) - if (next === undefined) { - await legacyStore.removeItem(legacyKey).catch(() => undefined) - continue - } - await current.setItem(key, next) - await legacyStore.removeItem(legacyKey) - return next - } - - return null - }, - setItem: async (key, value) => { - await current.setItem(key, value) - }, - removeItem: async (key) => { - await current.removeItem(key) - }, - } - - return api - })() - - const [state, setState, init] = makePersisted(store, { name: config.key, storage }) - - const isAsync = init instanceof Promise - const [ready] = createResource( - () => init, - async (initValue) => { - if (initValue instanceof Promise) await initValue - return true - }, - { initialValue: !isAsync }, - ) - - return [ - state, - setState, - init, - Object.assign(() => (ready.loading ? false : ready.latest === true), { - promise: init instanceof Promise ? init : undefined, - }), - ] -} diff --git a/packages/app/src/utils/prompt.test.ts b/packages/app/src/utils/prompt.test.ts deleted file mode 100644 index 04768dde051..00000000000 --- a/packages/app/src/utils/prompt.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { Part } from "@kilocode/sdk/v2" -import { extractPromptFromParts } from "./prompt" - -describe("extractPromptFromParts", () => { - test("restores multiple uploaded attachments", () => { - const parts = [ - { - id: "text_1", - type: "text", - text: "check these", - sessionID: "ses_1", - messageID: "msg_1", - }, - { - id: "file_1", - type: "file", - mime: "image/png", - url: "data:image/png;base64,AAA", - filename: "a.png", - sessionID: "ses_1", - messageID: "msg_1", - }, - { - id: "file_2", - type: "file", - mime: "application/pdf", - url: "data:application/pdf;base64,BBB", - filename: "b.pdf", - sessionID: "ses_1", - messageID: "msg_1", - }, - ] satisfies Part[] - - const result = extractPromptFromParts(parts) - - expect(result).toHaveLength(3) - expect(result[0]).toMatchObject({ type: "text", content: "check these" }) - expect(result.slice(1)).toMatchObject([ - { type: "image", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" }, - { type: "image", filename: "b.pdf", mime: "application/pdf", dataUrl: "data:application/pdf;base64,BBB" }, - ]) - }) -}) diff --git a/packages/app/src/utils/prompt.ts b/packages/app/src/utils/prompt.ts deleted file mode 100644 index 2736d0e51a1..00000000000 --- a/packages/app/src/utils/prompt.ts +++ /dev/null @@ -1,203 +0,0 @@ -import type { AgentPart as MessageAgentPart, FilePart, Part, TextPart } from "@kilocode/sdk/v2" -import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt" - -type Inline = - | { - type: "file" - start: number - end: number - value: string - path: string - selection?: { - startLine: number - endLine: number - startChar: number - endChar: number - } - } - | { - type: "agent" - start: number - end: number - value: string - name: string - } - -function selectionFromFileUrl(url: string): Extract["selection"] { - const queryIndex = url.indexOf("?") - if (queryIndex === -1) return undefined - const params = new URLSearchParams(url.slice(queryIndex + 1)) - const startLine = Number(params.get("start")) - const endLine = Number(params.get("end")) - if (!Number.isFinite(startLine) || !Number.isFinite(endLine)) return undefined - return { - startLine, - endLine, - startChar: 0, - endChar: 0, - } -} - -function textPartValue(parts: Part[]) { - const candidates = parts - .filter((part): part is TextPart => part.type === "text") - .filter((part) => !part.synthetic && !part.ignored) - return candidates.reduce((best: TextPart | undefined, part) => { - if (!best) return part - if (part.text.length > best.text.length) return part - return best - }, undefined) -} - -/** - * Extract prompt content from message parts for restoring into the prompt input. - * This is used by undo to restore the original user prompt. - */ -export function extractPromptFromParts(parts: Part[], opts?: { directory?: string; attachmentName?: string }): Prompt { - const textPart = textPartValue(parts) - const text = textPart?.text ?? "" - const directory = opts?.directory - const attachmentName = opts?.attachmentName ?? "attachment" - - const toRelative = (path: string) => { - if (!directory) return path - - const prefix = directory.endsWith("/") ? directory : directory + "/" - if (path.startsWith(prefix)) return path.slice(prefix.length) - - if (path.startsWith(directory)) { - const next = path.slice(directory.length) - if (next.startsWith("/")) return next.slice(1) - return next - } - - return path - } - - const inline: Inline[] = [] - const images: ImageAttachmentPart[] = [] - - for (const part of parts) { - if (part.type === "file") { - const filePart = part as FilePart - const sourceText = filePart.source?.text - if (sourceText) { - const value = sourceText.value - const start = sourceText.start - const end = sourceText.end - let path = value - if (value.startsWith("@")) path = value.slice(1) - if (!value.startsWith("@") && filePart.source && "path" in filePart.source) { - path = filePart.source.path - } - inline.push({ - type: "file", - start, - end, - value, - path: toRelative(path), - selection: selectionFromFileUrl(filePart.url), - }) - continue - } - - if (filePart.url.startsWith("data:")) { - images.push({ - type: "image", - id: filePart.id, - filename: filePart.filename ?? attachmentName, - mime: filePart.mime, - dataUrl: filePart.url, - }) - } - } - - if (part.type === "agent") { - const agentPart = part as MessageAgentPart - const source = agentPart.source - if (!source) continue - inline.push({ - type: "agent", - start: source.start, - end: source.end, - value: source.value, - name: agentPart.name, - }) - } - } - - inline.sort((a, b) => { - if (a.start !== b.start) return a.start - b.start - return a.end - b.end - }) - - const result: Prompt = [] - let position = 0 - let cursor = 0 - - const pushText = (content: string) => { - if (!content) return - result.push({ - type: "text", - content, - start: position, - end: position + content.length, - }) - position += content.length - } - - const pushFile = (item: Extract) => { - const content = item.value - const attachment: FileAttachmentPart = { - type: "file", - path: item.path, - content, - start: position, - end: position + content.length, - selection: item.selection, - } - result.push(attachment) - position += content.length - } - - const pushAgent = (item: Extract) => { - const content = item.value - const mention: AgentPart = { - type: "agent", - name: item.name, - content, - start: position, - end: position + content.length, - } - result.push(mention) - position += content.length - } - - for (const item of inline) { - if (item.start < 0 || item.end < item.start) continue - - const expected = item.value - if (!expected) continue - - const mismatch = item.end > text.length || item.start < cursor || text.slice(item.start, item.end) !== expected - const start = mismatch ? text.indexOf(expected, cursor) : item.start - if (start === -1) continue - const end = mismatch ? start + expected.length : item.end - - pushText(text.slice(cursor, start)) - - if (item.type === "file") pushFile(item) - if (item.type === "agent") pushAgent(item) - - cursor = end - } - - pushText(text.slice(cursor)) - - if (result.length === 0) { - result.push({ type: "text", content: "", start: 0, end: 0 }) - } - - if (images.length === 0) return result - return [...result, ...images] -} diff --git a/packages/app/src/utils/runtime-adapters.test.ts b/packages/app/src/utils/runtime-adapters.test.ts deleted file mode 100644 index 49552e179c2..00000000000 --- a/packages/app/src/utils/runtime-adapters.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { - disposeIfDisposable, - getHoveredLinkText, - getSpeechRecognitionCtor, - hasSetOption, - isDisposable, - setOptionIfSupported, -} from "./runtime-adapters" - -describe("runtime adapters", () => { - test("detects and disposes disposable values", () => { - let count = 0 - const value = { - dispose: () => { - count += 1 - }, - } - expect(isDisposable(value)).toBe(true) - disposeIfDisposable(value) - expect(count).toBe(1) - }) - - test("ignores non-disposable values", () => { - expect(isDisposable({ dispose: "nope" })).toBe(false) - expect(() => disposeIfDisposable({ dispose: "nope" })).not.toThrow() - }) - - test("sets options only when setter exists", () => { - const calls: Array<[string, unknown]> = [] - const value = { - setOption: (key: string, next: unknown) => { - calls.push([key, next]) - }, - } - expect(hasSetOption(value)).toBe(true) - setOptionIfSupported(value, "fontFamily", "Berkeley Mono") - expect(calls).toEqual([["fontFamily", "Berkeley Mono"]]) - expect(() => setOptionIfSupported({}, "fontFamily", "Berkeley Mono")).not.toThrow() - }) - - test("reads hovered link text safely", () => { - expect(getHoveredLinkText({ currentHoveredLink: { text: "https://example.com" } })).toBe("https://example.com") - expect(getHoveredLinkText({ currentHoveredLink: { text: 1 } })).toBeUndefined() - expect(getHoveredLinkText(null)).toBeUndefined() - }) - - test("resolves speech recognition constructor with webkit precedence", () => { - // oxlint-disable-next-line no-extraneous-class - class SpeechCtor {} - // oxlint-disable-next-line no-extraneous-class - class WebkitCtor {} - const ctor = getSpeechRecognitionCtor({ - SpeechRecognition: SpeechCtor, - webkitSpeechRecognition: WebkitCtor, - }) - expect(ctor).toBe(WebkitCtor) - }) - - test("returns undefined when no valid speech constructor exists", () => { - expect(getSpeechRecognitionCtor({ SpeechRecognition: "nope" })).toBeUndefined() - expect(getSpeechRecognitionCtor(undefined)).toBeUndefined() - }) -}) diff --git a/packages/app/src/utils/runtime-adapters.ts b/packages/app/src/utils/runtime-adapters.ts deleted file mode 100644 index 4c74da5dc1d..00000000000 --- a/packages/app/src/utils/runtime-adapters.ts +++ /dev/null @@ -1,39 +0,0 @@ -type RecordValue = Record - -const isRecord = (value: unknown): value is RecordValue => { - return typeof value === "object" && value !== null -} - -export const isDisposable = (value: unknown): value is { dispose: () => void } => { - return isRecord(value) && typeof value.dispose === "function" -} - -export const disposeIfDisposable = (value: unknown) => { - if (!isDisposable(value)) return - value.dispose() -} - -export const hasSetOption = (value: unknown): value is { setOption: (key: string, next: unknown) => void } => { - return isRecord(value) && typeof value.setOption === "function" -} - -export const setOptionIfSupported = (value: unknown, key: string, next: unknown) => { - if (!hasSetOption(value)) return - value.setOption(key, next) -} - -export const getHoveredLinkText = (value: unknown) => { - if (!isRecord(value)) return - const link = value.currentHoveredLink - if (!isRecord(link)) return - if (typeof link.text !== "string") return - return link.text -} - -export const getSpeechRecognitionCtor = (value: unknown): (new () => T) | undefined => { - if (!isRecord(value)) return - const ctor = - typeof value.webkitSpeechRecognition === "function" ? value.webkitSpeechRecognition : value.SpeechRecognition - if (typeof ctor !== "function") return - return ctor as new () => T -} diff --git a/packages/app/src/utils/same.ts b/packages/app/src/utils/same.ts deleted file mode 100644 index c956f92998a..00000000000 --- a/packages/app/src/utils/same.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function same(a: readonly T[] | undefined, b: readonly T[] | undefined) { - if (a === b) return true - if (!a || !b) return false - if (a.length !== b.length) return false - return a.every((x, i) => x === b[i]) -} diff --git a/packages/app/src/utils/scoped-cache.test.ts b/packages/app/src/utils/scoped-cache.test.ts deleted file mode 100644 index 0c6189dafe5..00000000000 --- a/packages/app/src/utils/scoped-cache.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { createScopedCache } from "./scoped-cache" - -describe("createScopedCache", () => { - test("evicts least-recently-used entry when max is reached", () => { - const disposed: string[] = [] - const cache = createScopedCache((key) => ({ key }), { - maxEntries: 2, - dispose: (value) => disposed.push(value.key), - }) - - const a = cache.get("a") - const b = cache.get("b") - expect(a.key).toBe("a") - expect(b.key).toBe("b") - - cache.get("a") - const c = cache.get("c") - - expect(c.key).toBe("c") - expect(cache.peek("a")?.key).toBe("a") - expect(cache.peek("b")).toBeUndefined() - expect(cache.peek("c")?.key).toBe("c") - expect(disposed).toEqual(["b"]) - }) - - test("disposes entries on delete and clear", () => { - const disposed: string[] = [] - const cache = createScopedCache((key) => ({ key }), { - dispose: (value) => disposed.push(value.key), - }) - - cache.get("a") - cache.get("b") - - const removed = cache.delete("a") - expect(removed?.key).toBe("a") - expect(cache.peek("a")).toBeUndefined() - - cache.clear() - expect(cache.peek("b")).toBeUndefined() - expect(disposed).toEqual(["a", "b"]) - }) - - test("expires stale entries with ttl and recreates on get", () => { - let clock = 0 - let count = 0 - const disposed: string[] = [] - const cache = createScopedCache((key) => ({ key, count: ++count }), { - ttlMs: 10, - now: () => clock, - dispose: (value) => disposed.push(`${value.key}:${value.count}`), - }) - - const first = cache.get("a") - expect(first.count).toBe(1) - - clock = 9 - expect(cache.peek("a")?.count).toBe(1) - - clock = 11 - expect(cache.peek("a")).toBeUndefined() - expect(disposed).toEqual(["a:1"]) - - const second = cache.get("a") - expect(second.count).toBe(2) - expect(disposed).toEqual(["a:1"]) - }) -}) diff --git a/packages/app/src/utils/scoped-cache.ts b/packages/app/src/utils/scoped-cache.ts deleted file mode 100644 index 224c363c1eb..00000000000 --- a/packages/app/src/utils/scoped-cache.ts +++ /dev/null @@ -1,104 +0,0 @@ -type ScopedCacheOptions = { - maxEntries?: number - ttlMs?: number - dispose?: (value: T, key: string) => void - now?: () => number -} - -type Entry = { - value: T - touchedAt: number -} - -export function createScopedCache(createValue: (key: string) => T, options: ScopedCacheOptions = {}) { - const store = new Map>() - const now = options.now ?? Date.now - - const dispose = (key: string, entry: Entry) => { - options.dispose?.(entry.value, key) - } - - const expired = (entry: Entry) => { - if (options.ttlMs === undefined) return false - return now() - entry.touchedAt >= options.ttlMs - } - - const sweep = () => { - if (options.ttlMs === undefined) return - for (const [key, entry] of store) { - if (!expired(entry)) continue - store.delete(key) - dispose(key, entry) - } - } - - const touch = (key: string, entry: Entry) => { - entry.touchedAt = now() - store.delete(key) - store.set(key, entry) - } - - const prune = () => { - if (options.maxEntries === undefined) return - while (store.size > options.maxEntries) { - const key = store.keys().next().value - if (!key) return - const entry = store.get(key) - store.delete(key) - if (!entry) continue - dispose(key, entry) - } - } - - const remove = (key: string) => { - const entry = store.get(key) - if (!entry) return - store.delete(key) - dispose(key, entry) - return entry.value - } - - const peek = (key: string) => { - sweep() - const entry = store.get(key) - if (!entry) return - if (!expired(entry)) return entry.value - store.delete(key) - dispose(key, entry) - } - - const get = (key: string) => { - sweep() - const entry = store.get(key) - if (entry && !expired(entry)) { - touch(key, entry) - return entry.value - } - if (entry) { - store.delete(key) - dispose(key, entry) - } - - const created = { - value: createValue(key), - touchedAt: now(), - } - store.set(key, created) - prune() - return created.value - } - - const clear = () => { - for (const [key, entry] of store) { - dispose(key, entry) - } - store.clear() - } - - return { - get, - peek, - delete: remove, - clear, - } -} diff --git a/packages/app/src/utils/server-errors.test.ts b/packages/app/src/utils/server-errors.test.ts deleted file mode 100644 index 1f53bb8cf66..00000000000 --- a/packages/app/src/utils/server-errors.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { ConfigInvalidError, ProviderModelNotFoundError } from "./server-errors" -import { formatServerError, parseReadableConfigInvalidError } from "./server-errors" - -function fill(text: string, vars?: Record) { - if (!vars) return text - return text.replace(/{{\s*(\w+)\s*}}/g, (_, key: string) => { - const value = vars[key] - if (value === undefined) return "" - return String(value) - }) -} - -function useLanguageMock() { - const dict: Record = { - "error.chain.unknown": "Erro desconhecido", - "error.chain.configInvalid": "Arquivo de config em {{path}} invalido", - "error.chain.configInvalidWithMessage": "Arquivo de config em {{path}} invalido: {{message}}", - "error.chain.modelNotFound": "Modelo nao encontrado: {{provider}}/{{model}}", - "error.chain.didYouMean": "Voce quis dizer: {{suggestions}}", - "error.chain.checkConfig": "Revise provider/model no config", - } - return { - t(key: string, vars?: Record) { - const text = dict[key] - if (!text) return key - return fill(text, vars) - }, - } -} - -const language = useLanguageMock() - -describe("parseReadableConfigInvalidError", () => { - test("formats issues with file path", () => { - const error = { - name: "ConfigInvalidError", - data: { - path: "opencode.config.ts", - issues: [ - { path: ["settings", "host"], message: "Required" }, - { path: ["mode"], message: "Invalid" }, - ], - }, - } satisfies ConfigInvalidError - - const result = parseReadableConfigInvalidError(error, language.t) - - expect(result).toBe( - ["Arquivo de config em opencode.config.ts invalido: settings.host: Required", "mode: Invalid"].join("\n"), - ) - }) - - test("uses trimmed message when issues are missing", () => { - const error = { - name: "ConfigInvalidError", - data: { - path: "config", - message: " Bad value ", - }, - } satisfies ConfigInvalidError - - const result = parseReadableConfigInvalidError(error, language.t) - - expect(result).toBe("Arquivo de config em config invalido: Bad value") - }) -}) - -describe("formatServerError", () => { - test("formats config invalid errors", () => { - const error = { - name: "ConfigInvalidError", - data: { - message: "Missing host", - }, - } satisfies ConfigInvalidError - - const result = formatServerError(error, language.t) - - expect(result).toBe("Arquivo de config em config invalido: Missing host") - }) - - test("returns error messages", () => { - expect(formatServerError(new Error("Request failed with status 503"), language.t)).toBe( - "Request failed with status 503", - ) - }) - - test("returns provided string errors", () => { - expect(formatServerError("Failed to connect to server", language.t)).toBe("Failed to connect to server") - }) - - test("uses translated unknown fallback", () => { - expect(formatServerError(0, language.t)).toBe("Erro desconhecido") - }) - - test("falls back for unknown error objects and names", () => { - expect(formatServerError({ name: "ServerTimeoutError", data: { seconds: 30 } }, language.t)).toBe( - "Erro desconhecido", - ) - }) - - test("formats provider model errors using provider/model", () => { - const error = { - name: "ProviderModelNotFoundError", - data: { - providerID: "openai", - modelID: "gpt-4.1", - }, - } satisfies ProviderModelNotFoundError - - expect(formatServerError(error, language.t)).toBe( - ["Modelo nao encontrado: openai/gpt-4.1", "Revise provider/model no config"].join("\n"), - ) - }) - - test("formats provider model suggestions", () => { - const error = { - name: "ProviderModelNotFoundError", - data: { - providerID: "x", - modelID: "y", - suggestions: ["x/y2", "x/y3"], - }, - } satisfies ProviderModelNotFoundError - - expect(formatServerError(error, language.t)).toBe( - ["Modelo nao encontrado: x/y", "Voce quis dizer: x/y2, x/y3", "Revise provider/model no config"].join("\n"), - ) - }) -}) diff --git a/packages/app/src/utils/server-errors.ts b/packages/app/src/utils/server-errors.ts deleted file mode 100644 index 2c3a8c54dbb..00000000000 --- a/packages/app/src/utils/server-errors.ts +++ /dev/null @@ -1,80 +0,0 @@ -export type ConfigInvalidError = { - name: "ConfigInvalidError" - data: { - path?: string - message?: string - issues?: Array<{ message: string; path: string[] }> - } -} - -export type ProviderModelNotFoundError = { - name: "ProviderModelNotFoundError" - data: { - providerID: string - modelID: string - suggestions?: string[] - } -} - -type Translator = (key: string, vars?: Record) => string - -function tr(translator: Translator | undefined, key: string, text: string, vars?: Record) { - if (!translator) return text - const out = translator(key, vars) - if (!out || out === key) return text - return out -} - -export function formatServerError(error: unknown, translate?: Translator, fallback?: string) { - if (isConfigInvalidErrorLike(error)) return parseReadableConfigInvalidError(error, translate) - if (isProviderModelNotFoundErrorLike(error)) return parseReadableProviderModelNotFoundError(error, translate) - if (error instanceof Error && error.message) return error.message - if (typeof error === "string" && error) return error - if (fallback) return fallback - return tr(translate, "error.chain.unknown", "Unknown error") -} - -function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError { - if (typeof error !== "object" || error === null) return false - const o = error as Record - return o.name === "ConfigInvalidError" && typeof o.data === "object" && o.data !== null -} - -function isProviderModelNotFoundErrorLike(error: unknown): error is ProviderModelNotFoundError { - if (typeof error !== "object" || error === null) return false - const o = error as Record - return o.name === "ProviderModelNotFoundError" && typeof o.data === "object" && o.data !== null -} - -export function parseReadableConfigInvalidError(errorInput: ConfigInvalidError, translator?: Translator) { - const file = errorInput.data.path && errorInput.data.path !== "config" ? errorInput.data.path : "config" - const detail = errorInput.data.message?.trim() ?? "" - const issues = (errorInput.data.issues ?? []) - .map((issue) => { - const msg = issue.message.trim() - if (!issue.path.length) return msg - return `${issue.path.join(".")}: ${msg}` - }) - .filter(Boolean) - const msg = issues.length ? issues.join("\n") : detail - if (!msg) return tr(translator, "error.chain.configInvalid", `Config file at ${file} is invalid`, { path: file }) - return tr(translator, "error.chain.configInvalidWithMessage", `Config file at ${file} is invalid: ${msg}`, { - path: file, - message: msg, - }) -} - -function parseReadableProviderModelNotFoundError(errorInput: ProviderModelNotFoundError, translator?: Translator) { - const p = errorInput.data.providerID.trim() - const m = errorInput.data.modelID.trim() - const list = (errorInput.data.suggestions ?? []).map((v) => v.trim()).filter(Boolean) - const body = tr(translator, "error.chain.modelNotFound", `Model not found: ${p}/${m}`, { provider: p, model: m }) - const tail = tr(translator, "error.chain.checkConfig", "Check your config (opencode.json) provider/model names") - if (list.length) { - const suggestions = list.slice(0, 5).join(", ") - return [body, tr(translator, "error.chain.didYouMean", `Did you mean: ${suggestions}`, { suggestions }), tail].join( - "\n", - ) - } - return [body, tail].join("\n") -} diff --git a/packages/app/src/utils/server-health.test.ts b/packages/app/src/utils/server-health.test.ts deleted file mode 100644 index 50082dcf35d..00000000000 --- a/packages/app/src/utils/server-health.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { ServerConnection } from "@/context/server" -import { checkServerHealth } from "./server-health" - -const server: ServerConnection.HttpBase = { - url: "http://localhost:4096", -} - -function abortFromInput(input: RequestInfo | URL, init?: RequestInit) { - if (init?.signal) return init.signal - if (input instanceof Request) return input.signal - return undefined -} - -describe("checkServerHealth", () => { - test("returns healthy response with version", async () => { - const fetch = (async () => - new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), { - status: 200, - headers: { "content-type": "application/json" }, - })) as unknown as typeof globalThis.fetch - - const result = await checkServerHealth(server, fetch) - - expect(result).toEqual({ healthy: true, version: "1.2.3" }) - }) - - test("returns unhealthy when request fails", async () => { - const fetch = (async () => { - throw new Error("network") - }) as unknown as typeof globalThis.fetch - - const result = await checkServerHealth(server, fetch) - - expect(result).toEqual({ healthy: false }) - }) - - test("uses timeout fallback when AbortSignal.timeout is unavailable", async () => { - const timeout = Object.getOwnPropertyDescriptor(AbortSignal, "timeout") - Object.defineProperty(AbortSignal, "timeout", { - configurable: true, - value: undefined, - }) - - let aborted = false - const fetch = ((input: RequestInfo | URL, init?: RequestInit) => - new Promise((_resolve, reject) => { - const signal = abortFromInput(input, init) - signal?.addEventListener( - "abort", - () => { - aborted = true - reject(new DOMException("Aborted", "AbortError")) - }, - { once: true }, - ) - })) as unknown as typeof globalThis.fetch - - const result = await checkServerHealth(server, fetch, { - timeoutMs: 10, - }).finally(() => { - if (timeout) Object.defineProperty(AbortSignal, "timeout", timeout) - if (!timeout) Reflect.deleteProperty(AbortSignal, "timeout") - }) - - expect(aborted).toBe(true) - expect(result).toEqual({ healthy: false }) - }) - - test("uses provided abort signal", async () => { - let signal: AbortSignal | undefined - const fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { - signal = abortFromInput(input, init) - return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), { - status: 200, - headers: { "content-type": "application/json" }, - }) - }) as unknown as typeof globalThis.fetch - - const abort = new AbortController() - await checkServerHealth(server, fetch, { - signal: abort.signal, - }) - - expect(signal).toBe(abort.signal) - }) - - test("retries transient failures and eventually succeeds", async () => { - let count = 0 - const fetch = (async () => { - count += 1 - if (count < 3) throw new TypeError("network") - return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), { - status: 200, - headers: { "content-type": "application/json" }, - }) - }) as unknown as typeof globalThis.fetch - - const result = await checkServerHealth(server, fetch, { - retryCount: 2, - retryDelayMs: 1, - }) - - expect(count).toBe(3) - expect(result).toEqual({ healthy: true, version: "1.2.3" }) - }) - - test("returns unhealthy when retries are exhausted", async () => { - let count = 0 - const fetch = (async () => { - count += 1 - throw new TypeError("network") - }) as unknown as typeof globalThis.fetch - - const result = await checkServerHealth(server, fetch, { - retryCount: 2, - retryDelayMs: 1, - }) - - expect(count).toBe(3) - expect(result).toEqual({ healthy: false }) - }) -}) diff --git a/packages/app/src/utils/server-health.ts b/packages/app/src/utils/server-health.ts deleted file mode 100644 index a13fd34ef72..00000000000 --- a/packages/app/src/utils/server-health.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { usePlatform } from "@/context/platform" -import type { ServerConnection } from "@/context/server" -import { createSdkForServer } from "./server" - -export type ServerHealth = { healthy: boolean; version?: string } - -interface CheckServerHealthOptions { - timeoutMs?: number - signal?: AbortSignal - retryCount?: number - retryDelayMs?: number -} - -const defaultTimeoutMs = 3000 -const defaultRetryCount = 2 -const defaultRetryDelayMs = 100 -const cacheMs = 750 -const healthCache = new Map< - string, - { at: number; done: boolean; fetch: typeof globalThis.fetch; promise: Promise } ->() - -function cacheKey(server: ServerConnection.HttpBase) { - return `${server.url}\n${server.username ?? ""}\n${server.password ?? ""}` -} - -function timeoutSignal(timeoutMs: number) { - const timeout = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout - if (timeout) { - try { - return { - signal: timeout.call(AbortSignal, timeoutMs), - clear: undefined as (() => void) | undefined, - } - } catch {} - } - const controller = new AbortController() - const timer = setTimeout(() => controller.abort(), timeoutMs) - return { signal: controller.signal, clear: () => clearTimeout(timer) } -} - -function wait(ms: number, signal?: AbortSignal) { - return new Promise((resolve, reject) => { - if (signal?.aborted) { - reject(new DOMException("Aborted", "AbortError")) - return - } - const timer = setTimeout(() => { - signal?.removeEventListener("abort", onAbort) - resolve() - }, ms) - const onAbort = () => { - clearTimeout(timer) - reject(new DOMException("Aborted", "AbortError")) - } - signal?.addEventListener("abort", onAbort, { once: true }) - }) -} - -function retryable(error: unknown, signal?: AbortSignal) { - if (signal?.aborted) return false - if (!(error instanceof Error)) return false - if (error.name === "AbortError" || error.name === "TimeoutError") return false - if (error instanceof TypeError) return true - return /network|fetch|econnreset|econnrefused|enotfound|timedout/i.test(error.message) -} - -export async function checkServerHealth( - server: ServerConnection.HttpBase, - fetch: typeof globalThis.fetch, - opts?: CheckServerHealthOptions, -): Promise { - const timeout = opts?.signal ? undefined : timeoutSignal(opts?.timeoutMs ?? defaultTimeoutMs) - const signal = opts?.signal ?? timeout?.signal - const retryCount = opts?.retryCount ?? defaultRetryCount - const retryDelayMs = opts?.retryDelayMs ?? defaultRetryDelayMs - const next = (count: number, error: unknown) => { - if (count >= retryCount || !retryable(error, signal)) return Promise.resolve({ healthy: false } as const) - return wait(retryDelayMs * (count + 1), signal) - .then(() => attempt(count + 1)) - .catch(() => ({ healthy: false })) - } - const attempt = (count: number): Promise => - createSdkForServer({ - server, - fetch, - signal, - }) - .global.health() - .then((x) => (x.error ? next(count, x.error) : { healthy: x.data?.healthy === true, version: x.data?.version })) - .catch((error) => next(count, error)) - return attempt(0).finally(() => timeout?.clear?.()) -} - -export function useCheckServerHealth() { - const platform = usePlatform() - const fetcher = platform.fetch ?? globalThis.fetch - - return (http: ServerConnection.HttpBase) => { - const key = cacheKey(http) - const hit = healthCache.get(key) - const now = Date.now() - if (hit && hit.fetch === fetcher && (!hit.done || now - hit.at < cacheMs)) return hit.promise - const promise = checkServerHealth(http, fetcher).finally(() => { - const next = healthCache.get(key) - if (!next || next.promise !== promise) return - next.done = true - next.at = Date.now() - }) - healthCache.set(key, { at: now, done: false, fetch: fetcher, promise }) - return promise - } -} diff --git a/packages/app/src/utils/server.ts b/packages/app/src/utils/server.ts deleted file mode 100644 index 139641882c5..00000000000 --- a/packages/app/src/utils/server.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createKiloClient } from "@kilocode/sdk/v2/client" -import type { ServerConnection } from "@/context/server" - -export function createSdkForServer({ - server, - ...config -}: Omit[0]>, "baseUrl"> & { - server: ServerConnection.HttpBase -}) { - const auth = (() => { - if (!server.password) return - return { - Authorization: `Basic ${btoa(`${server.username ?? "kilo"}:${server.password}`)}`, - } - })() - - return createKiloClient({ - ...config, - headers: { - ...(config.headers instanceof Headers ? Object.fromEntries(config.headers.entries()) : config.headers), - ...auth, - }, - baseUrl: server.url, - }) -} diff --git a/packages/app/src/utils/session-title.ts b/packages/app/src/utils/session-title.ts deleted file mode 100644 index ca04c010470..00000000000 --- a/packages/app/src/utils/session-title.ts +++ /dev/null @@ -1,7 +0,0 @@ -const pattern = /^(New session|Child session) - \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ - -export function sessionTitle(title?: string) { - if (!title) return title - const match = title.match(pattern) - return match?.[1] ?? title -} diff --git a/packages/app/src/utils/solid-dnd.tsx b/packages/app/src/utils/solid-dnd.tsx deleted file mode 100644 index 8e30a033aec..00000000000 --- a/packages/app/src/utils/solid-dnd.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useDragDropContext } from "@thisbeyond/solid-dnd" -import type { Transformer } from "@thisbeyond/solid-dnd" -import { createRoot, onCleanup, type JSXElement } from "solid-js" - -type DragEvent = { draggable?: { id?: unknown } } - -const isDragEvent = (event: unknown): event is DragEvent => { - if (typeof event !== "object" || event === null) return false - return "draggable" in event -} - -export const getDraggableId = (event: unknown): string | undefined => { - if (!isDragEvent(event)) return undefined - const draggable = event.draggable - if (!draggable) return undefined - return typeof draggable.id === "string" ? draggable.id : undefined -} - -const createTransformer = (id: string, axis: "x" | "y"): Transformer => ({ - id, - order: 100, - callback: (transform) => (axis === "x" ? { ...transform, x: 0 } : { ...transform, y: 0 }), -}) - -const createAxisConstraint = (axis: "x" | "y", transformerId: string) => (): JSXElement => { - const context = useDragDropContext() - if (!context) return null - const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context - const transformer = createTransformer(transformerId, axis) - const dispose = createRoot((dispose) => { - onDragStart((event) => { - const id = getDraggableId(event) - if (!id) return - addTransformer("draggables", id, transformer) - }) - onDragEnd((event) => { - const id = getDraggableId(event) - if (!id) return - removeTransformer("draggables", id, transformer.id) - }) - return dispose - }) - onCleanup(dispose) - return null -} - -export const ConstrainDragXAxis = createAxisConstraint("x", "constrain-x-axis") - -export const ConstrainDragYAxis = createAxisConstraint("y", "constrain-y-axis") diff --git a/packages/app/src/utils/sound.ts b/packages/app/src/utils/sound.ts deleted file mode 100644 index 78e5a0c565e..00000000000 --- a/packages/app/src/utils/sound.ts +++ /dev/null @@ -1,102 +0,0 @@ -let files: Record Promise> | undefined -let loads: Record Promise> | undefined - -function getFiles() { - if (files) return files - files = import.meta.glob("../../../ui/src/assets/audio/*.aac", { import: "default" }) as Record< - string, - () => Promise - > - return files -} - -export const SOUND_OPTIONS = [ - { id: "alert-01", label: "sound.option.alert01" }, - { id: "alert-02", label: "sound.option.alert02" }, - { id: "alert-03", label: "sound.option.alert03" }, - { id: "alert-04", label: "sound.option.alert04" }, - { id: "alert-05", label: "sound.option.alert05" }, - { id: "alert-06", label: "sound.option.alert06" }, - { id: "alert-07", label: "sound.option.alert07" }, - { id: "alert-08", label: "sound.option.alert08" }, - { id: "alert-09", label: "sound.option.alert09" }, - { id: "alert-10", label: "sound.option.alert10" }, - { id: "bip-bop-01", label: "sound.option.bipbop01" }, - { id: "bip-bop-02", label: "sound.option.bipbop02" }, - { id: "bip-bop-03", label: "sound.option.bipbop03" }, - { id: "bip-bop-04", label: "sound.option.bipbop04" }, - { id: "bip-bop-05", label: "sound.option.bipbop05" }, - { id: "bip-bop-06", label: "sound.option.bipbop06" }, - { id: "bip-bop-07", label: "sound.option.bipbop07" }, - { id: "bip-bop-08", label: "sound.option.bipbop08" }, - { id: "bip-bop-09", label: "sound.option.bipbop09" }, - { id: "bip-bop-10", label: "sound.option.bipbop10" }, - { id: "staplebops-01", label: "sound.option.staplebops01" }, - { id: "staplebops-02", label: "sound.option.staplebops02" }, - { id: "staplebops-03", label: "sound.option.staplebops03" }, - { id: "staplebops-04", label: "sound.option.staplebops04" }, - { id: "staplebops-05", label: "sound.option.staplebops05" }, - { id: "staplebops-06", label: "sound.option.staplebops06" }, - { id: "staplebops-07", label: "sound.option.staplebops07" }, - { id: "nope-01", label: "sound.option.nope01" }, - { id: "nope-02", label: "sound.option.nope02" }, - { id: "nope-03", label: "sound.option.nope03" }, - { id: "nope-04", label: "sound.option.nope04" }, - { id: "nope-05", label: "sound.option.nope05" }, - { id: "nope-06", label: "sound.option.nope06" }, - { id: "nope-07", label: "sound.option.nope07" }, - { id: "nope-08", label: "sound.option.nope08" }, - { id: "nope-09", label: "sound.option.nope09" }, - { id: "nope-10", label: "sound.option.nope10" }, - { id: "nope-11", label: "sound.option.nope11" }, - { id: "nope-12", label: "sound.option.nope12" }, - { id: "yup-01", label: "sound.option.yup01" }, - { id: "yup-02", label: "sound.option.yup02" }, - { id: "yup-03", label: "sound.option.yup03" }, - { id: "yup-04", label: "sound.option.yup04" }, - { id: "yup-05", label: "sound.option.yup05" }, - { id: "yup-06", label: "sound.option.yup06" }, -] as const - -export type SoundOption = (typeof SOUND_OPTIONS)[number] -export type SoundID = SoundOption["id"] - -function getLoads() { - if (loads) return loads - loads = Object.fromEntries( - Object.entries(getFiles()).flatMap(([path, load]) => { - const file = path.split("/").at(-1) - if (!file) return [] - return [[file.replace(/\.aac$/, ""), load] as const] - }), - ) as Record Promise> - return loads -} - -const cache = new Map>() - -export function soundSrc(id: string | undefined) { - const loads = getLoads() - if (!id || !(id in loads)) return Promise.resolve(undefined) - const key = id as SoundID - const hit = cache.get(key) - if (hit) return hit - const next = loads[key]().catch(() => undefined) - cache.set(key, next) - return next -} - -export function playSound(src: string | undefined) { - if (typeof Audio === "undefined") return - if (!src) return - const audio = new Audio(src) - audio.play().catch(() => undefined) - return () => { - audio.pause() - audio.currentTime = 0 - } -} - -export function playSoundById(id: string | undefined) { - return soundSrc(id).then((src) => playSound(src)) -} diff --git a/packages/app/src/utils/terminal-writer.test.ts b/packages/app/src/utils/terminal-writer.test.ts deleted file mode 100644 index c49702e39b1..00000000000 --- a/packages/app/src/utils/terminal-writer.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { terminalWriter } from "./terminal-writer" - -describe("terminalWriter", () => { - test("buffers and flushes once per schedule", () => { - const calls: string[] = [] - const scheduled: VoidFunction[] = [] - const writer = terminalWriter( - (data, done) => { - calls.push(data) - done?.() - }, - (flush) => scheduled.push(flush), - ) - - writer.push("a") - writer.push("b") - writer.push("c") - - expect(calls).toEqual([]) - expect(scheduled).toHaveLength(1) - - scheduled[0]?.() - expect(calls).toEqual(["abc"]) - }) - - test("flush is a no-op when empty", () => { - const calls: string[] = [] - const writer = terminalWriter( - (data, done) => { - calls.push(data) - done?.() - }, - (flush) => flush(), - ) - writer.flush() - expect(calls).toEqual([]) - }) - - test("flush waits for pending write completion", () => { - const calls: string[] = [] - let done: VoidFunction | undefined - const writer = terminalWriter( - (data, finish) => { - calls.push(data) - done = finish - }, - (flush) => flush(), - ) - - writer.push("a") - - let settled = false - writer.flush(() => { - settled = true - }) - - expect(calls).toEqual(["a"]) - expect(settled).toBe(false) - - done?.() - expect(settled).toBe(true) - }) -}) diff --git a/packages/app/src/utils/terminal-writer.ts b/packages/app/src/utils/terminal-writer.ts deleted file mode 100644 index 083f51de471..00000000000 --- a/packages/app/src/utils/terminal-writer.ts +++ /dev/null @@ -1,65 +0,0 @@ -export function terminalWriter( - write: (data: string, done?: VoidFunction) => void, - schedule: (flush: VoidFunction) => void = queueMicrotask, -) { - let chunks: string[] | undefined - let waits: VoidFunction[] | undefined - let scheduled = false - let writing = false - - const settle = () => { - if (scheduled || writing || chunks?.length) return - const list = waits - if (!list?.length) return - waits = undefined - for (const fn of list) { - fn() - } - } - - const run = () => { - if (writing) return - scheduled = false - const items = chunks - if (!items?.length) { - settle() - return - } - chunks = undefined - writing = true - write(items.join(""), () => { - writing = false - if (chunks?.length) { - if (scheduled) return - scheduled = true - schedule(run) - return - } - settle() - }) - } - - const push = (data: string) => { - if (!data) return - if (chunks) chunks.push(data) - else chunks = [data] - - if (scheduled || writing) return - scheduled = true - schedule(run) - } - - const flush = (done?: VoidFunction) => { - if (!scheduled && !writing && !chunks?.length) { - done?.() - return - } - if (done) { - if (waits) waits.push(done) - else waits = [done] - } - run() - } - - return { push, flush } -} diff --git a/packages/app/src/utils/time.ts b/packages/app/src/utils/time.ts deleted file mode 100644 index d183e10807d..00000000000 --- a/packages/app/src/utils/time.ts +++ /dev/null @@ -1,22 +0,0 @@ -type TimeKey = - | "common.time.justNow" - | "common.time.minutesAgo.short" - | "common.time.hoursAgo.short" - | "common.time.daysAgo.short" - -type Translate = (key: TimeKey, params?: Record) => string - -export function getRelativeTime(dateString: string, t: Translate): string { - const date = new Date(dateString) - const now = new Date() - const diffMs = now.getTime() - date.getTime() - const diffSeconds = Math.floor(diffMs / 1000) - const diffMinutes = Math.floor(diffSeconds / 60) - const diffHours = Math.floor(diffMinutes / 60) - const diffDays = Math.floor(diffHours / 24) - - if (diffSeconds < 60) return t("common.time.justNow") - if (diffMinutes < 60) return t("common.time.minutesAgo.short", { count: diffMinutes }) - if (diffHours < 24) return t("common.time.hoursAgo.short", { count: diffHours }) - return t("common.time.daysAgo.short", { count: diffDays }) -} diff --git a/packages/app/src/utils/uuid.test.ts b/packages/app/src/utils/uuid.test.ts deleted file mode 100644 index e6b4e282409..00000000000 --- a/packages/app/src/utils/uuid.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test" -import { uuid } from "./uuid" - -const cryptoDescriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto") -const secureDescriptor = Object.getOwnPropertyDescriptor(globalThis, "isSecureContext") -const randomDescriptor = Object.getOwnPropertyDescriptor(Math, "random") - -const setCrypto = (value: Partial) => { - Object.defineProperty(globalThis, "crypto", { - configurable: true, - value: value as Crypto, - }) -} - -const setSecure = (value: boolean) => { - Object.defineProperty(globalThis, "isSecureContext", { - configurable: true, - value, - }) -} - -const setRandom = (value: () => number) => { - Object.defineProperty(Math, "random", { - configurable: true, - value, - }) -} - -afterEach(() => { - if (cryptoDescriptor) { - Object.defineProperty(globalThis, "crypto", cryptoDescriptor) - } - - if (secureDescriptor) { - Object.defineProperty(globalThis, "isSecureContext", secureDescriptor) - } - - if (!secureDescriptor) { - delete (globalThis as { isSecureContext?: boolean }).isSecureContext - } - - if (randomDescriptor) { - Object.defineProperty(Math, "random", randomDescriptor) - } -}) - -describe("uuid", () => { - test("uses randomUUID in secure contexts", () => { - setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" }) - setSecure(true) - expect(uuid()).toBe("00000000-0000-0000-0000-000000000000") - }) - - test("falls back in insecure contexts", () => { - setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" }) - setSecure(false) - setRandom(() => 0.5) - expect(uuid()).toBe("8") - }) - - test("falls back when randomUUID throws", () => { - setCrypto({ - randomUUID: () => { - throw new DOMException("Failed", "OperationError") - }, - }) - setSecure(true) - setRandom(() => 0.5) - expect(uuid()).toBe("8") - }) - - test("falls back when randomUUID is unavailable", () => { - setCrypto({}) - setSecure(true) - setRandom(() => 0.5) - expect(uuid()).toBe("8") - }) -}) diff --git a/packages/app/src/utils/uuid.ts b/packages/app/src/utils/uuid.ts deleted file mode 100644 index 7b964068c86..00000000000 --- a/packages/app/src/utils/uuid.ts +++ /dev/null @@ -1,12 +0,0 @@ -const fallback = () => Math.random().toString(16).slice(2) - -export function uuid() { - const c = globalThis.crypto - if (!c || typeof c.randomUUID !== "function") return fallback() - if (typeof globalThis.isSecureContext === "boolean" && !globalThis.isSecureContext) return fallback() - try { - return c.randomUUID() - } catch { - return fallback() - } -} diff --git a/packages/app/src/utils/worktree.test.ts b/packages/app/src/utils/worktree.test.ts deleted file mode 100644 index 8161e7ad836..00000000000 --- a/packages/app/src/utils/worktree.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { Worktree } from "./worktree" - -const dir = (name: string) => `/tmp/opencode-worktree-${name}-${crypto.randomUUID()}` - -describe("Worktree", () => { - test("normalizes trailing slashes", () => { - const key = dir("normalize") - Worktree.ready(`${key}/`) - - expect(Worktree.get(key)).toEqual({ status: "ready" }) - }) - - test("pending does not overwrite a terminal state", () => { - const key = dir("pending") - Worktree.failed(key, "boom") - Worktree.pending(key) - - expect(Worktree.get(key)).toEqual({ status: "failed", message: "boom" }) - }) - - test("wait resolves shared pending waiter when ready", async () => { - const key = dir("wait-ready") - Worktree.pending(key) - - const a = Worktree.wait(key) - const b = Worktree.wait(`${key}/`) - - expect(a).toBe(b) - - Worktree.ready(key) - - expect(await a).toEqual({ status: "ready" }) - expect(await b).toEqual({ status: "ready" }) - }) - - test("wait resolves with failure message", async () => { - const key = dir("wait-failed") - const waiting = Worktree.wait(key) - - Worktree.failed(key, "permission denied") - - expect(await waiting).toEqual({ status: "failed", message: "permission denied" }) - expect(await Worktree.wait(key)).toEqual({ status: "failed", message: "permission denied" }) - }) -}) diff --git a/packages/app/src/utils/worktree.ts b/packages/app/src/utils/worktree.ts deleted file mode 100644 index 581afd5535e..00000000000 --- a/packages/app/src/utils/worktree.ts +++ /dev/null @@ -1,73 +0,0 @@ -const normalize = (directory: string) => directory.replace(/[\\/]+$/, "") - -type State = - | { - status: "pending" - } - | { - status: "ready" - } - | { - status: "failed" - message: string - } - -const state = new Map() -const waiters = new Map< - string, - { - promise: Promise - resolve: (state: State) => void - } ->() - -function deferred() { - const box = { resolve: (_: State) => {} } - const promise = new Promise((resolve) => { - box.resolve = resolve - }) - return { promise, resolve: box.resolve } -} - -export const Worktree = { - get(directory: string) { - return state.get(normalize(directory)) - }, - pending(directory: string) { - const key = normalize(directory) - const current = state.get(key) - if (current && current.status !== "pending") return - state.set(key, { status: "pending" }) - }, - ready(directory: string) { - const key = normalize(directory) - const next = { status: "ready" } as const - state.set(key, next) - const waiter = waiters.get(key) - if (!waiter) return - waiters.delete(key) - waiter.resolve(next) - }, - failed(directory: string, message: string) { - const key = normalize(directory) - const next = { status: "failed", message } as const - state.set(key, next) - const waiter = waiters.get(key) - if (!waiter) return - waiters.delete(key) - waiter.resolve(next) - }, - wait(directory: string) { - const key = normalize(directory) - const current = state.get(key) - if (current && current.status !== "pending") return Promise.resolve(current) - - const existing = waiters.get(key) - if (existing) return existing.promise - - const waiter = deferred() - - waiters.set(key, waiter) - return waiter.promise - }, -} diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json deleted file mode 100644 index 74db2030324..00000000000 --- a/packages/app/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "compilerOptions": { - "composite": true, - "target": "ESNext", - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "jsx": "preserve", - "jsxImportSource": "solid-js", - "allowJs": true, - "resolveJsonModule": true, - "strict": true, - "noEmit": false, - "emitDeclarationOnly": true, - "outDir": "node_modules/.ts-dist", - "isolatedModules": true, - "types": ["bun", "node"], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src", "package.json"], - "exclude": ["dist", "ts-dist"] -} diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts deleted file mode 100644 index 6a29ae6345e..00000000000 --- a/packages/app/vite.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from "vite" -import desktopPlugin from "./vite" - -export default defineConfig({ - plugins: [desktopPlugin] as any, - server: { - host: "0.0.0.0", - allowedHosts: true, - port: 3000, - }, - build: { - target: "esnext", - // sourcemap: true, - }, -}) diff --git a/packages/app/vite.js b/packages/app/vite.js deleted file mode 100644 index bdbf32c0916..00000000000 --- a/packages/app/vite.js +++ /dev/null @@ -1,38 +0,0 @@ -import { readFileSync } from "node:fs" -import solidPlugin from "vite-plugin-solid" -import tailwindcss from "@tailwindcss/vite" -import { fileURLToPath } from "url" - -const theme = fileURLToPath(new URL("./public/oc-theme-preload.js", import.meta.url)) - -/** - * @type {import("vite").PluginOption} - */ -export default [ - { - name: "kilo-desktop:config", - config() { - return { - resolve: { - alias: { - "@": fileURLToPath(new URL("./src", import.meta.url)), - }, - }, - worker: { - format: "es", - }, - } - }, - }, - { - name: "opencode-desktop:theme-preload", - transformIndexHtml(html) { - return html.replace( - '', - ``, - ) - }, - }, - tailwindcss(), - solidPlugin(), -] diff --git a/packages/console/core/migrations/20260417071612_tidy_diamondback/snapshot.json b/packages/console/core/migrations/20260417071612_tidy_diamondback/snapshot.json deleted file mode 100644 index 2152bfa76fe..00000000000 --- a/packages/console/core/migrations/20260417071612_tidy_diamondback/snapshot.json +++ /dev/null @@ -1,2567 +0,0 @@ -{ - "version": "6", - "dialect": "mysql", - "id": "93c492af-c95b-4213-9fc2-38c3dd10374d", - "prevIds": ["a09a925d-6cdd-4e7c-b8b1-11c259928b4c"], - "ddl": [ - { - "name": "account", - "entityType": "tables" - }, - { - "name": "auth", - "entityType": "tables" - }, - { - "name": "benchmark", - "entityType": "tables" - }, - { - "name": "billing", - "entityType": "tables" - }, - { - "name": "lite", - "entityType": "tables" - }, - { - "name": "payment", - "entityType": "tables" - }, - { - "name": "subscription", - "entityType": "tables" - }, - { - "name": "usage", - "entityType": "tables" - }, - { - "name": "ip_rate_limit", - "entityType": "tables" - }, - { - "name": "ip", - "entityType": "tables" - }, - { - "name": "key_rate_limit", - "entityType": "tables" - }, - { - "name": "model_rate_limit", - "entityType": "tables" - }, - { - "name": "key", - "entityType": "tables" - }, - { - "name": "model", - "entityType": "tables" - }, - { - "name": "provider", - "entityType": "tables" - }, - { - "name": "user", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(now())", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_deleted", - "entityType": "columns", - "table": "account" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "auth" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(now())", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "auth" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "auth" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_deleted", - "entityType": "columns", - "table": "auth" - }, - { - "type": "enum('email','github','google')", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "provider", - "entityType": "columns", - "table": "auth" - }, - { - "type": "varchar(255)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "subject", - "entityType": "columns", - "table": "auth" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "account_id", - "entityType": "columns", - "table": "auth" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "benchmark" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(now())", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "benchmark" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "benchmark" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_deleted", - "entityType": "columns", - "table": "benchmark" - }, - { - "type": "varchar(64)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "benchmark" - }, - { - "type": "varchar(64)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "benchmark" - }, - { - "type": "mediumtext", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "result", - "entityType": "columns", - "table": "benchmark" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "billing" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "billing" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(now())", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "billing" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "billing" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_deleted", - "entityType": "columns", - "table": "billing" - }, - { - "type": "varchar(255)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "customer_id", - "entityType": "columns", - "table": "billing" - }, - { - "type": "varchar(255)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "payment_method_id", - "entityType": "columns", - "table": "billing" - }, - { - "type": "varchar(32)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "payment_method_type", - "entityType": "columns", - "table": "billing" - }, - { - "type": "varchar(4)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "payment_method_last4", - "entityType": "columns", - "table": "billing" - }, - { - "type": "bigint", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "balance", - "entityType": "columns", - "table": "billing" - }, - { - "type": "int", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "monthly_limit", - "entityType": "columns", - "table": "billing" - }, - { - "type": "bigint", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "monthly_usage", - "entityType": "columns", - "table": "billing" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_monthly_usage_updated", - "entityType": "columns", - "table": "billing" - }, - { - "type": "boolean", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "reload", - "entityType": "columns", - "table": "billing" - }, - { - "type": "int", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "reload_trigger", - "entityType": "columns", - "table": "billing" - }, - { - "type": "int", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "reload_amount", - "entityType": "columns", - "table": "billing" - }, - { - "type": "varchar(255)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "reload_error", - "entityType": "columns", - "table": "billing" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_reload_error", - "entityType": "columns", - "table": "billing" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_reload_locked_till", - "entityType": "columns", - "table": "billing" - }, - { - "type": "json", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "subscription", - "entityType": "columns", - "table": "billing" - }, - { - "type": "varchar(28)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "subscription_id", - "entityType": "columns", - "table": "billing" - }, - { - "type": "enum('20','100','200')", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "subscription_plan", - "entityType": "columns", - "table": "billing" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_subscription_booked", - "entityType": "columns", - "table": "billing" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_subscription_selected", - "entityType": "columns", - "table": "billing" - }, - { - "type": "varchar(28)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "lite_subscription_id", - "entityType": "columns", - "table": "billing" - }, - { - "type": "json", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "lite", - "entityType": "columns", - "table": "billing" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "lite" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "lite" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(now())", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "lite" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "lite" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_deleted", - "entityType": "columns", - "table": "lite" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "user_id", - "entityType": "columns", - "table": "lite" - }, - { - "type": "bigint", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "rolling_usage", - "entityType": "columns", - "table": "lite" - }, - { - "type": "bigint", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "weekly_usage", - "entityType": "columns", - "table": "lite" - }, - { - "type": "bigint", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "monthly_usage", - "entityType": "columns", - "table": "lite" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_rolling_updated", - "entityType": "columns", - "table": "lite" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_weekly_updated", - "entityType": "columns", - "table": "lite" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_monthly_updated", - "entityType": "columns", - "table": "lite" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "payment" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "payment" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(now())", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "payment" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "payment" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_deleted", - "entityType": "columns", - "table": "payment" - }, - { - "type": "varchar(255)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "customer_id", - "entityType": "columns", - "table": "payment" - }, - { - "type": "varchar(255)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "invoice_id", - "entityType": "columns", - "table": "payment" - }, - { - "type": "varchar(255)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "payment_id", - "entityType": "columns", - "table": "payment" - }, - { - "type": "bigint", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "amount", - "entityType": "columns", - "table": "payment" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_refunded", - "entityType": "columns", - "table": "payment" - }, - { - "type": "json", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "enrichment", - "entityType": "columns", - "table": "payment" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "subscription" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "subscription" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(now())", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "subscription" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "subscription" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_deleted", - "entityType": "columns", - "table": "subscription" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "user_id", - "entityType": "columns", - "table": "subscription" - }, - { - "type": "bigint", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "rolling_usage", - "entityType": "columns", - "table": "subscription" - }, - { - "type": "bigint", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "fixed_usage", - "entityType": "columns", - "table": "subscription" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_rolling_updated", - "entityType": "columns", - "table": "subscription" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_fixed_updated", - "entityType": "columns", - "table": "subscription" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "usage" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "usage" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(now())", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "usage" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "usage" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_deleted", - "entityType": "columns", - "table": "usage" - }, - { - "type": "varchar(255)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "usage" - }, - { - "type": "varchar(255)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "provider", - "entityType": "columns", - "table": "usage" - }, - { - "type": "int", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "input_tokens", - "entityType": "columns", - "table": "usage" - }, - { - "type": "int", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "output_tokens", - "entityType": "columns", - "table": "usage" - }, - { - "type": "int", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "reasoning_tokens", - "entityType": "columns", - "table": "usage" - }, - { - "type": "int", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "cache_read_tokens", - "entityType": "columns", - "table": "usage" - }, - { - "type": "int", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "cache_write_5m_tokens", - "entityType": "columns", - "table": "usage" - }, - { - "type": "int", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "cache_write_1h_tokens", - "entityType": "columns", - "table": "usage" - }, - { - "type": "bigint", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "usage" - }, - { - "type": "varchar(30)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "key_id", - "entityType": "columns", - "table": "usage" - }, - { - "type": "varchar(30)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "usage" - }, - { - "type": "json", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "enrichment", - "entityType": "columns", - "table": "usage" - }, - { - "type": "varchar(45)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "ip", - "entityType": "columns", - "table": "ip_rate_limit" - }, - { - "type": "varchar(10)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "interval", - "entityType": "columns", - "table": "ip_rate_limit" - }, - { - "type": "int", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "count", - "entityType": "columns", - "table": "ip_rate_limit" - }, - { - "type": "varchar(45)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "ip", - "entityType": "columns", - "table": "ip" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(now())", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "ip" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "ip" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_deleted", - "entityType": "columns", - "table": "ip" - }, - { - "type": "int", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "usage", - "entityType": "columns", - "table": "ip" - }, - { - "type": "varchar(255)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "key", - "entityType": "columns", - "table": "key_rate_limit" - }, - { - "type": "varchar(40)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "interval", - "entityType": "columns", - "table": "key_rate_limit" - }, - { - "type": "int", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "count", - "entityType": "columns", - "table": "key_rate_limit" - }, - { - "type": "varchar(255)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "key", - "entityType": "columns", - "table": "model_rate_limit" - }, - { - "type": "varchar(40)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "interval", - "entityType": "columns", - "table": "model_rate_limit" - }, - { - "type": "int", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "count", - "entityType": "columns", - "table": "model_rate_limit" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "key" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "key" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(now())", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "key" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "key" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_deleted", - "entityType": "columns", - "table": "key" - }, - { - "type": "varchar(255)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "key" - }, - { - "type": "varchar(255)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "key", - "entityType": "columns", - "table": "key" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "user_id", - "entityType": "columns", - "table": "key" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "key" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "model" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "model" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(now())", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "model" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "model" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_deleted", - "entityType": "columns", - "table": "model" - }, - { - "type": "varchar(64)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "model" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "provider" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "provider" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(now())", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "provider" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "provider" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_deleted", - "entityType": "columns", - "table": "provider" - }, - { - "type": "varchar(64)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "provider", - "entityType": "columns", - "table": "provider" - }, - { - "type": "text", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "credentials", - "entityType": "columns", - "table": "provider" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "user" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "user" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(now())", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "user" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "user" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_deleted", - "entityType": "columns", - "table": "user" - }, - { - "type": "varchar(30)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "account_id", - "entityType": "columns", - "table": "user" - }, - { - "type": "varchar(255)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "user" - }, - { - "type": "varchar(255)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "user" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_seen", - "entityType": "columns", - "table": "user" - }, - { - "type": "int", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "color", - "entityType": "columns", - "table": "user" - }, - { - "type": "enum('admin','member')", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "role", - "entityType": "columns", - "table": "user" - }, - { - "type": "int", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "monthly_limit", - "entityType": "columns", - "table": "user" - }, - { - "type": "bigint", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "monthly_usage", - "entityType": "columns", - "table": "user" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_monthly_usage_updated", - "entityType": "columns", - "table": "user" - }, - { - "type": "varchar(30)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "varchar(255)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "varchar(255)", - "notNull": true, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(now())", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "timestamp(3)", - "notNull": true, - "autoIncrement": false, - "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))", - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "timestamp(3)", - "notNull": false, - "autoIncrement": false, - "default": null, - "onUpdateNow": false, - "onUpdateNowFsp": null, - "charSet": null, - "collation": null, - "generated": null, - "name": "time_deleted", - "entityType": "columns", - "table": "workspace" - }, - { - "columns": ["id"], - "name": "PRIMARY", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "name": "PRIMARY", - "table": "auth", - "entityType": "pks" - }, - { - "columns": ["id"], - "name": "PRIMARY", - "table": "benchmark", - "entityType": "pks" - }, - { - "columns": ["workspace_id", "id"], - "name": "PRIMARY", - "table": "billing", - "entityType": "pks" - }, - { - "columns": ["workspace_id", "id"], - "name": "PRIMARY", - "table": "lite", - "entityType": "pks" - }, - { - "columns": ["workspace_id", "id"], - "name": "PRIMARY", - "table": "payment", - "entityType": "pks" - }, - { - "columns": ["workspace_id", "id"], - "name": "PRIMARY", - "table": "subscription", - "entityType": "pks" - }, - { - "columns": ["workspace_id", "id"], - "name": "PRIMARY", - "table": "usage", - "entityType": "pks" - }, - { - "columns": ["ip", "interval"], - "name": "PRIMARY", - "table": "ip_rate_limit", - "entityType": "pks" - }, - { - "columns": ["ip"], - "name": "PRIMARY", - "table": "ip", - "entityType": "pks" - }, - { - "columns": ["key", "interval"], - "name": "PRIMARY", - "table": "key_rate_limit", - "entityType": "pks" - }, - { - "columns": ["key", "interval"], - "name": "PRIMARY", - "table": "model_rate_limit", - "entityType": "pks" - }, - { - "columns": ["workspace_id", "id"], - "name": "PRIMARY", - "table": "key", - "entityType": "pks" - }, - { - "columns": ["workspace_id", "id"], - "name": "PRIMARY", - "table": "model", - "entityType": "pks" - }, - { - "columns": ["workspace_id", "id"], - "name": "PRIMARY", - "table": "provider", - "entityType": "pks" - }, - { - "columns": ["workspace_id", "id"], - "name": "PRIMARY", - "table": "user", - "entityType": "pks" - }, - { - "columns": ["id"], - "name": "PRIMARY", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "provider", - "isExpression": false - }, - { - "value": "subject", - "isExpression": false - } - ], - "isUnique": true, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "provider", - "entityType": "indexes", - "table": "auth" - }, - { - "columns": [ - { - "value": "account_id", - "isExpression": false - } - ], - "isUnique": false, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "account_id", - "entityType": "indexes", - "table": "auth" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "time_created", - "entityType": "indexes", - "table": "benchmark" - }, - { - "columns": [ - { - "value": "customer_id", - "isExpression": false - } - ], - "isUnique": true, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "global_customer_id", - "entityType": "indexes", - "table": "billing" - }, - { - "columns": [ - { - "value": "subscription_id", - "isExpression": false - } - ], - "isUnique": true, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "global_subscription_id", - "entityType": "indexes", - "table": "billing" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - }, - { - "value": "user_id", - "isExpression": false - } - ], - "isUnique": true, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "workspace_user_id", - "entityType": "indexes", - "table": "lite" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - }, - { - "value": "user_id", - "isExpression": false - } - ], - "isUnique": true, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "workspace_user_id", - "entityType": "indexes", - "table": "subscription" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "usage_time_created", - "entityType": "indexes", - "table": "usage" - }, - { - "columns": [ - { - "value": "key", - "isExpression": false - } - ], - "isUnique": true, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "global_key", - "entityType": "indexes", - "table": "key" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - }, - { - "value": "model", - "isExpression": false - } - ], - "isUnique": true, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "model_workspace_model", - "entityType": "indexes", - "table": "model" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - }, - { - "value": "provider", - "isExpression": false - } - ], - "isUnique": true, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "workspace_provider", - "entityType": "indexes", - "table": "provider" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - }, - { - "value": "account_id", - "isExpression": false - } - ], - "isUnique": true, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "user_account_id", - "entityType": "indexes", - "table": "user" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - }, - { - "value": "email", - "isExpression": false - } - ], - "isUnique": true, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "user_email", - "entityType": "indexes", - "table": "user" - }, - { - "columns": [ - { - "value": "account_id", - "isExpression": false - } - ], - "isUnique": false, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "global_account_id", - "entityType": "indexes", - "table": "user" - }, - { - "columns": [ - { - "value": "email", - "isExpression": false - } - ], - "isUnique": false, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "global_email", - "entityType": "indexes", - "table": "user" - }, - { - "columns": [ - { - "value": "slug", - "isExpression": false - } - ], - "isUnique": true, - "using": null, - "algorithm": null, - "lock": null, - "nameExplicit": true, - "name": "slug", - "entityType": "indexes", - "table": "workspace" - } - ], - "renames": [] -} diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000000..4f5b23705da --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "version": "7.2.42", + "name": "@opencode-ai/core", + "type": "module", + "license": "MIT", + "private": true, + "scripts": { + "test": "bun test", + "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", + "typecheck": "tsgo --noEmit" + }, + "bin": { + "opencode": "./bin/opencode" + }, + "exports": { + "./*": "./src/*.ts" + }, + "imports": {}, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/cross-spawn": "catalog:", + "@types/npm-package-arg": "6.1.4", + "@types/npmcli__arborist": "6.3.3", + "@types/semver": "catalog:" + }, + "dependencies": { + "@effect/opentelemetry": "catalog:", + "@effect/platform-node": "catalog:", + "@npmcli/arborist": "9.4.0", + "@npmcli/config": "10.8.1", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/context-async-hooks": "2.6.1", + "@opentelemetry/exporter-trace-otlp-http": "0.214.0", + "@opentelemetry/sdk-trace-base": "2.6.1", + "effect": "catalog:", + "cross-spawn": "catalog:", + "glob": "13.0.5", + "mime-types": "3.0.2", + "minimatch": "10.2.5", + "npm-package-arg": "13.0.2", + "rotating-file-stream": "3.2.9", + "semver": "^7.6.3", + "xdg-basedir": "5.1.0", + "zod": "catalog:" + }, + "overrides": { + "drizzle-orm": "catalog:" + }, + "peerDependencies": {} +} diff --git a/packages/opencode/src/effect/cross-spawn-spawner.ts b/packages/core/src/cross-spawn-spawner.ts similarity index 97% rename from packages/opencode/src/effect/cross-spawn-spawner.ts rename to packages/core/src/cross-spawn-spawner.ts index 5e25263a08b..ad8d4126d45 100644 --- a/packages/opencode/src/effect/cross-spawn-spawner.ts +++ b/packages/core/src/cross-spawn-spawner.ts @@ -502,13 +502,4 @@ export const layer: Layer.Layer { - // Dynamic import to avoid circular dep: cross-spawn-spawner → run-service → Instance → project → cross-spawn-spawner - const { makeRuntime } = await import("@/effect/run-service") - return makeRuntime(ChildProcessSpawner, defaultLayer) -}) - -type RT = Awaited> -export const runPromiseExit: RT["runPromiseExit"] = async (...args) => (await rt()).runPromiseExit(...(args as [any])) +export * as CrossSpawnSpawner from "./cross-spawn-spawner" diff --git a/packages/opencode/src/effect/logger.ts b/packages/core/src/effect/logger.ts similarity index 98% rename from packages/opencode/src/effect/logger.ts rename to packages/core/src/effect/logger.ts index 0e58b8acb4b..69f9631e06b 100644 --- a/packages/opencode/src/effect/logger.ts +++ b/packages/core/src/effect/logger.ts @@ -1,5 +1,5 @@ import { Cause, Effect, Logger, References } from "effect" -import { Log } from "@/util" +import * as Log from "../util/log" type Fields = Record diff --git a/packages/opencode/src/effect/memo-map.ts b/packages/core/src/effect/memo-map.ts similarity index 100% rename from packages/opencode/src/effect/memo-map.ts rename to packages/core/src/effect/memo-map.ts diff --git a/packages/opencode/src/effect/observability.ts b/packages/core/src/effect/observability.ts similarity index 92% rename from packages/opencode/src/effect/observability.ts rename to packages/core/src/effect/observability.ts index d8e58791e40..d9c24369b7d 100644 --- a/packages/opencode/src/effect/observability.ts +++ b/packages/core/src/effect/observability.ts @@ -2,9 +2,9 @@ import { Effect, Layer, Logger } from "effect" import { FetchHttpClient } from "effect/unstable/http" import { OtlpLogger, OtlpSerialization } from "effect/unstable/observability" import * as EffectLogger from "./logger" -import { Flag } from "@/flag/flag" -import { InstallationChannel, InstallationVersion } from "@/installation/version" -import { ensureProcessMetadata } from "@/util/opencode-process" +import { Flag } from "../flag/flag" +import { InstallationChannel, InstallationVersion } from "../installation/version" +import { ensureProcessMetadata } from "../util/opencode-process" const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT export const enabled = !!base @@ -76,7 +76,7 @@ const traces = async () => { // register(), so the global @opentelemetry/api context manager stays // as the no-op default. Non-Effect code (like the AI SDK) that calls // tracer.startActiveSpan() relies on context.active() to find the - // parent span — without a real context manager every span starts a + // parent span - without a real context manager every span starts a // new trace. Registering AsyncLocalStorageContextManager fixes this. const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks") const { context } = await import("@opentelemetry/api") diff --git a/packages/opencode/src/effect/runtime.ts b/packages/core/src/effect/runtime.ts similarity index 94% rename from packages/opencode/src/effect/runtime.ts rename to packages/core/src/effect/runtime.ts index ad7872f0b56..e4f68270989 100644 --- a/packages/opencode/src/effect/runtime.ts +++ b/packages/core/src/effect/runtime.ts @@ -1,11 +1,13 @@ -import { Observability } from "./observability" import { Layer, type Context, ManagedRuntime, type Effect } from "effect" import { memoMap } from "./memo-map" +import { Observability } from "./observability" export function makeRuntime(service: Context.Service, layer: Layer.Layer) { let rt: ManagedRuntime.ManagedRuntime | undefined const getRuntime = () => - (rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer) as Layer.Layer, { memoMap })) + (rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer) as Layer.Layer, { + memoMap, + })) return { runSync: (fn: (svc: S) => Effect.Effect) => getRuntime().runSync(service.use(fn)), diff --git a/packages/shared/src/filesystem.ts b/packages/core/src/filesystem.ts similarity index 100% rename from packages/shared/src/filesystem.ts rename to packages/core/src/filesystem.ts diff --git a/packages/opencode/src/flag/flag.ts b/packages/core/src/flag/flag.ts similarity index 100% rename from packages/opencode/src/flag/flag.ts rename to packages/core/src/flag/flag.ts diff --git a/packages/opencode/src/global/index.ts b/packages/core/src/global.ts similarity index 57% rename from packages/opencode/src/global/index.ts rename to packages/core/src/global.ts index fb44048c0a5..a514d48d93b 100644 --- a/packages/opencode/src/global/index.ts +++ b/packages/core/src/global.ts @@ -1,12 +1,12 @@ +import path from "path" import fs from "fs/promises" import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" -import path from "path" import os from "os" -import { Filesystem } from "../util" -import { Flock } from "@opencode-ai/shared/util/flock" +import { Context, Effect, Layer } from "effect" +import { Flock } from "./util/flock" +import { markNoIndex } from "./kilocode/spotlight" // kilocode_change const app = "kilo" // kilocode_change - // kilocode_change start // Defensively strip newline characters from the resolved XDG paths. // If `$HOME` (or any `$XDG_*_HOME` override) has a trailing newline in @@ -22,10 +22,9 @@ const config = path.join(clean(xdgConfig)!, app) const state = path.join(clean(xdgState)!, app) // kilocode_change end -export const Path = { - // Allow override via KILO_TEST_HOME for test isolation +const paths = { get home() { - return (process.env.KILO_TEST_HOME || os.homedir()).trim() // kilocode_change — defensive trim, see above + return (process.env.KILO_TEST_HOME ?? os.homedir()).trim() // kilocode_change — defensive trim, see above }, data, bin: path.join(cache, "bin"), @@ -35,7 +34,8 @@ export const Path = { state, } -// Initialize Flock with global state path +export const Path = paths + Flock.setGlobal({ state }) await Promise.all([ @@ -46,23 +46,35 @@ await Promise.all([ fs.mkdir(Path.bin, { recursive: true }), ]) -const CACHE_VERSION = "21" +// kilocode_change start - keep generated Kilo data out of macOS Spotlight +await Promise.all([Path.data, Path.cache, Path.state].map(markNoIndex)) +// kilocode_change end -const version = await Filesystem.readText(path.join(Path.cache, "version")).catch(() => "0") +export class Service extends Context.Service()("@opencode/Global") {} -if (version !== CACHE_VERSION) { - try { - const contents = await fs.readdir(Path.cache) - await Promise.all( - contents.map((item) => - fs.rm(path.join(Path.cache, item), { - recursive: true, - force: true, - }), - ), - ) - } catch {} - await Filesystem.write(path.join(Path.cache, "version"), CACHE_VERSION) +export interface Interface { + readonly home: string + readonly data: string + readonly cache: string + readonly config: string + readonly state: string + readonly bin: string + readonly log: string } -export * as Global from "." +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + return Service.of({ + home: Path.home, + data: Path.data, + cache: Path.cache, + config: Path.config, + state: Path.state, + bin: Path.bin, + log: Path.log, + }) + }), +) + +export * as Global from "./global" diff --git a/packages/opencode/src/installation/version.ts b/packages/core/src/installation/version.ts similarity index 100% rename from packages/opencode/src/installation/version.ts rename to packages/core/src/installation/version.ts diff --git a/packages/core/src/kilocode/spotlight.ts b/packages/core/src/kilocode/spotlight.ts new file mode 100644 index 00000000000..ce6e494361f --- /dev/null +++ b/packages/core/src/kilocode/spotlight.ts @@ -0,0 +1,23 @@ +import fs from "fs/promises" +import path from "path" + +const marker = ".metadata_never_index" + +function exists(err: unknown): boolean { + if (typeof err !== "object" || err === null) return false + return "code" in err && err.code === "EEXIST" +} + +function message(err: unknown): string { + if (err instanceof Error) return err.message + return String(err) +} + +export async function markNoIndex(dir: string): Promise { + if (process.platform !== "darwin") return + const file = path.join(dir, marker) + await fs.writeFile(file, "", { flag: "wx" }).catch((err) => { + if (exists(err)) return + process.emitWarning(`Failed to mark ${dir} as Spotlight-excluded: ${message(err)}`) + }) +} diff --git a/packages/core/src/npm-config.ts b/packages/core/src/npm-config.ts new file mode 100644 index 00000000000..896bb84872c --- /dev/null +++ b/packages/core/src/npm-config.ts @@ -0,0 +1,40 @@ +export * as NpmConfig from "./npm-config" + +import { fileURLToPath } from "url" +// @ts-expect-error npm does not publish types for this internal config API. +import Config from "@npmcli/config" +// @ts-expect-error npm does not publish types for this internal config API. +import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js" +import { Effect } from "effect" + +const npmPath = fileURLToPath(new URL("..", import.meta.url)) + +export const load = (dir: string) => + Effect.tryPromise({ + try: async () => { + const config = new Config({ + npmPath, + cwd: dir, + env: { ...process.env }, + argv: [process.execPath, process.execPath], + execPath: process.execPath, + platform: process.platform, + definitions, + flatten, + nerfDarts, + shorthands, + warn: false, + }) + await config.load() + return config.flat as Record + }, + catch: (cause) => cause, + }).pipe(Effect.orElseSucceed(() => ({}) as Record)) + +export const registry = (dir: string) => + load(dir).pipe( + Effect.map((config) => { + const registry = typeof config.registry === "string" ? config.registry : "https://registry.npmjs.org" + return registry.endsWith("/") ? registry.slice(0, -1) : registry + }), + ) diff --git a/packages/opencode/src/npm/index.ts b/packages/core/src/npm.ts similarity index 72% rename from packages/opencode/src/npm/index.ts rename to packages/core/src/npm.ts index 4b1f8070709..92e40427681 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/core/src/npm.ts @@ -1,20 +1,14 @@ -export * as Npm from "." +export * as Npm from "./npm" import path from "path" -import { fileURLToPath } from "url" import npa from "npm-package-arg" -import semver from "semver" -import Config from "@npmcli/config" -import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js" -import { Effect, Schema, Context, Layer, Option, FileSystem, Stream } from "effect" +import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect" import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Global } from "@opencode-ai/shared/global" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" - -import * as CrossSpawnSpawner from "../effect/cross-spawn-spawner" -import { makeRuntime } from "../effect/runtime" +import { AppFileSystem } from "./filesystem" +import { Global } from "./global" +import { EffectFlock } from "./util/effect-flock" +import { makeRuntime } from "./effect/runtime" +import { NpmConfig } from "./npm-config" export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { add: Schema.Array(Schema.String).pipe(Schema.optional), @@ -38,46 +32,18 @@ export interface Interface { }[] }, ) => Effect.Effect - readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect readonly which: (pkg: string, bin?: string) => Effect.Effect> } export class Service extends Context.Service()("@opencode/Npm") {} const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined -const npmPath = fileURLToPath(new URL("../..", import.meta.url)) export function sanitize(pkg: string) { if (!illegal) return pkg return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("") } -const loadOptions = (dir: string) => - Effect.tryPromise({ - try: async () => { - const config = new Config({ - npmPath, - cwd: dir, - env: { ...process.env }, - argv: [process.execPath, process.execPath], - execPath: process.execPath, - platform: process.platform, - definitions, - flatten, - nerfDarts, - shorthands, - warn: false, - }) - await config.load() - return config.flat - }, - catch: (cause) => - new InstallFailedError({ - cause, - dir, - }), - }) - const resolveEntryPoint = (name: string, dir: string): EntryPoint => { let entrypoint: Option.Option try { @@ -108,39 +74,13 @@ export const layer = Layer.effect( const global = yield* Global.Service const fs = yield* FileSystem.FileSystem const flock = yield* EffectFlock.Service - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg)) - const runView = Effect.fnUntraced(function* (cmd: string[]) { - const handle = yield* spawner.spawn( - ChildProcess.make(cmd[0], cmd.slice(1), { - extendEnv: true, - }), - ) - const [stdout, stderr] = yield* Effect.all( - [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], - { concurrency: 2 }, - ) - const code = yield* handle.exitCode - if (code !== 0 || !stdout.trim()) { - return yield* Effect.fail(stderr || stdout || `Failed to run ${cmd.join(" ")}`) - } - return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String))(stdout) - }, Effect.scoped) - const viewLatestVersion = Effect.fnUntraced(function* (pkg: string) { - return yield* runView(["npm", "view", pkg, "dist-tags.latest", "--json"]).pipe( - Effect.catch(() => - runView(["pnpm", "view", pkg, "dist-tags.latest", "--json"]).pipe( - Effect.catch(() => runView(["bun", "pm", "view", pkg, "dist-tags.latest", "--json"])), - ), - ), - ) - }) const reify = (input: { dir: string; add?: string[] }) => Effect.gen(function* () { yield* flock.acquire(`npm-install:${input.dir}`) const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist")) const add = input.add ?? [] - const npmOptions = yield* loadOptions(input.dir) + const npmOptions = yield* NpmConfig.load(input.dir) const arborist = new Arborist({ ...npmOptions, path: input.dir, @@ -170,18 +110,6 @@ export const layer = Layer.effect( }), ) - const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) { - const latestVersion = yield* viewLatestVersion(pkg).pipe(Effect.option) - if (Option.isNone(latestVersion)) { - return false - } - - const range = /[\s^~*xX<>|=]/.test(cachedVersion) - if (range) return !semver.satisfies(latestVersion.value, cachedVersion) - - return semver.lt(cachedVersion, latestVersion.value) - }) - const add = Effect.fn("Npm.add")(function* (pkg: string) { const dir = directory(pkg) const name = (() => { @@ -307,7 +235,6 @@ export const layer = Layer.effect( return Service.of({ add, install, - outdated, which, }) }), @@ -318,7 +245,6 @@ export const defaultLayer = layer.pipe( Layer.provide(AppFileSystem.layer), Layer.provide(Global.layer), Layer.provide(NodeFileSystem.layer), - Layer.provide(CrossSpawnSpawner.defaultLayer), ) const { runPromise } = makeRuntime(Service, defaultLayer) @@ -335,10 +261,6 @@ export async function add(...args: Parameters) { } } -export async function outdated(...args: Parameters) { - return runPromise((svc) => svc.outdated(...args)) -} - export async function which(...args: Parameters) { const resolved = await runPromise((svc) => svc.which(...args)) return Option.getOrUndefined(resolved) diff --git a/packages/shared/src/util/array.ts b/packages/core/src/util/array.ts similarity index 100% rename from packages/shared/src/util/array.ts rename to packages/core/src/util/array.ts diff --git a/packages/shared/src/util/binary.ts b/packages/core/src/util/binary.ts similarity index 100% rename from packages/shared/src/util/binary.ts rename to packages/core/src/util/binary.ts diff --git a/packages/shared/src/util/effect-flock.ts b/packages/core/src/util/effect-flock.ts similarity index 100% rename from packages/shared/src/util/effect-flock.ts rename to packages/core/src/util/effect-flock.ts diff --git a/packages/shared/src/util/encode.ts b/packages/core/src/util/encode.ts similarity index 100% rename from packages/shared/src/util/encode.ts rename to packages/core/src/util/encode.ts diff --git a/packages/shared/src/util/error.ts b/packages/core/src/util/error.ts similarity index 100% rename from packages/shared/src/util/error.ts rename to packages/core/src/util/error.ts diff --git a/packages/shared/src/util/flock.ts b/packages/core/src/util/flock.ts similarity index 100% rename from packages/shared/src/util/flock.ts rename to packages/core/src/util/flock.ts diff --git a/packages/shared/src/util/glob.ts b/packages/core/src/util/glob.ts similarity index 100% rename from packages/shared/src/util/glob.ts rename to packages/core/src/util/glob.ts diff --git a/packages/shared/src/util/hash.ts b/packages/core/src/util/hash.ts similarity index 100% rename from packages/shared/src/util/hash.ts rename to packages/core/src/util/hash.ts diff --git a/packages/shared/src/util/identifier.ts b/packages/core/src/util/identifier.ts similarity index 100% rename from packages/shared/src/util/identifier.ts rename to packages/core/src/util/identifier.ts diff --git a/packages/shared/src/util/iife.ts b/packages/core/src/util/iife.ts similarity index 100% rename from packages/shared/src/util/iife.ts rename to packages/core/src/util/iife.ts diff --git a/packages/shared/src/util/lazy.ts b/packages/core/src/util/lazy.ts similarity index 100% rename from packages/shared/src/util/lazy.ts rename to packages/core/src/util/lazy.ts diff --git a/packages/opencode/src/util/log.ts b/packages/core/src/util/log.ts similarity index 98% rename from packages/opencode/src/util/log.ts rename to packages/core/src/util/log.ts index 15af0f97cb4..1181317e082 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/core/src/util/log.ts @@ -1,9 +1,9 @@ import path from "path" import { existsSync, writeFileSync } from "fs" // kilocode_change import fs from "fs/promises" -import { Global } from "../global" +import * as Global from "../global" import z from "zod" -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "./glob" import { createStream } from "rotating-file-stream" // kilocode_change export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) diff --git a/packages/shared/src/util/module.ts b/packages/core/src/util/module.ts similarity index 100% rename from packages/shared/src/util/module.ts rename to packages/core/src/util/module.ts diff --git a/packages/opencode/src/util/opencode-process.ts b/packages/core/src/util/opencode-process.ts similarity index 100% rename from packages/opencode/src/util/opencode-process.ts rename to packages/core/src/util/opencode-process.ts diff --git a/packages/shared/src/util/path.ts b/packages/core/src/util/path.ts similarity index 100% rename from packages/shared/src/util/path.ts rename to packages/core/src/util/path.ts diff --git a/packages/shared/src/util/retry.ts b/packages/core/src/util/retry.ts similarity index 100% rename from packages/shared/src/util/retry.ts rename to packages/core/src/util/retry.ts diff --git a/packages/shared/src/util/slug.ts b/packages/core/src/util/slug.ts similarity index 100% rename from packages/shared/src/util/slug.ts rename to packages/core/src/util/slug.ts diff --git a/packages/app/sst-env.d.ts b/packages/core/sst-env.d.ts similarity index 100% rename from packages/app/sst-env.d.ts rename to packages/core/sst-env.d.ts diff --git a/packages/opencode/test/effect/cross-spawn-spawner.test.ts b/packages/core/test/effect/cross-spawn-spawner.test.ts similarity index 96% rename from packages/opencode/test/effect/cross-spawn-spawner.test.ts rename to packages/core/test/effect/cross-spawn-spawner.test.ts index 49360d10b01..627db8ef36b 100644 --- a/packages/opencode/test/effect/cross-spawn-spawner.test.ts +++ b/packages/core/test/effect/cross-spawn-spawner.test.ts @@ -1,11 +1,11 @@ import { describe, expect } from "bun:test" import fs from "node:fs/promises" +import os from "node:os" import path from "node:path" import { Effect, Exit, Stream } from "effect" import type * as PlatformError from "effect/PlatformError" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" -import { tmpdir } from "../fixture/fixture" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { testEffect } from "../lib/effect" const live = CrossSpawnSpawner.defaultLayer @@ -39,11 +39,21 @@ function alive(pid: number) { } } +async function tmpdir() { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-test-")) + return { + path: dir, + async [Symbol.asyncDispose]() { + await fs.rm(dir, { recursive: true, force: true }) + }, + } +} + async function gone(pid: number, timeout = 5_000) { const end = Date.now() + timeout while (Date.now() < end) { if (!alive(pid)) return true - await Bun.sleep(50) + await new Promise((resolve) => setTimeout(resolve, 50)) } return !alive(pid) } @@ -395,7 +405,7 @@ describe("cross-spawn spawner", () => { const file = path.join(dir, "echo cmd.cmd") yield* Effect.promise(() => fs.mkdir(dir, { recursive: true })) - yield* Effect.promise(() => Bun.write(file, "@echo off\r\nif %~1==--stdio exit /b 0\r\nexit /b 7\r\n")) + yield* Effect.promise(() => fs.writeFile(file, "@echo off\r\nif %~1==--stdio exit /b 0\r\nexit /b 7\r\n")) const code = yield* ChildProcessSpawner.ChildProcessSpawner.use((svc) => svc.exitCode( diff --git a/packages/opencode/test/effect/observability.test.ts b/packages/core/test/effect/observability.test.ts similarity index 96% rename from packages/opencode/test/effect/observability.test.ts rename to packages/core/test/effect/observability.test.ts index cb61fe878a9..effadc584e1 100644 --- a/packages/opencode/test/effect/observability.test.ts +++ b/packages/core/test/effect/observability.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test" -import { resource } from "../../src/effect/observability" +import { resource } from "@opencode-ai/core/effect/observability" const otelResourceAttributes = process.env.OTEL_RESOURCE_ATTRIBUTES const opencodeClient = process.env.KILO_CLIENT diff --git a/packages/shared/test/filesystem/filesystem.test.ts b/packages/core/test/filesystem/filesystem.test.ts similarity index 99% rename from packages/shared/test/filesystem/filesystem.test.ts rename to packages/core/test/filesystem/filesystem.test.ts index b49026bcba9..b77f4e356fa 100644 --- a/packages/shared/test/filesystem/filesystem.test.ts +++ b/packages/core/test/filesystem/filesystem.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "bun:test" import { Effect, Layer, FileSystem } from "effect" import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { testEffect } from "../lib/effect" import path from "path" diff --git a/packages/shared/test/fixture/effect-flock-worker.ts b/packages/core/test/fixture/effect-flock-worker.ts similarity index 88% rename from packages/shared/test/fixture/effect-flock-worker.ts rename to packages/core/test/fixture/effect-flock-worker.ts index c9116c2d5c1..3dc3ee2c8b6 100644 --- a/packages/shared/test/fixture/effect-flock-worker.ts +++ b/packages/core/test/fixture/effect-flock-worker.ts @@ -1,9 +1,9 @@ import fs from "fs/promises" import os from "os" import { Effect, Layer } from "effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" -import { Global } from "@opencode-ai/shared/global" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { EffectFlock } from "@opencode-ai/core/util/effect-flock" +import { Global } from "@opencode-ai/core/global" type Msg = { key: string diff --git a/packages/shared/test/fixture/flock-worker.ts b/packages/core/test/fixture/flock-worker.ts similarity index 96% rename from packages/shared/test/fixture/flock-worker.ts rename to packages/core/test/fixture/flock-worker.ts index 9954d290cce..0b9c314c087 100644 --- a/packages/shared/test/fixture/flock-worker.ts +++ b/packages/core/test/fixture/flock-worker.ts @@ -1,5 +1,5 @@ import fs from "fs/promises" -import { Flock } from "@opencode-ai/shared/util/flock" +import { Flock } from "@opencode-ai/core/util/flock" type Msg = { key: string diff --git a/packages/core/test/fixture/tmpdir.ts b/packages/core/test/fixture/tmpdir.ts new file mode 100644 index 00000000000..950b1401b60 --- /dev/null +++ b/packages/core/test/fixture/tmpdir.ts @@ -0,0 +1,13 @@ +import fs from "fs/promises" +import { tmpdir as osTmpdir } from "os" +import path from "path" + +export const tmpdir = async () => { + const dir = await fs.mkdtemp(path.join(osTmpdir(), "opencode-core-test-")) + return { + path: dir, + async [Symbol.asyncDispose]() { + await fs.rm(dir, { recursive: true, force: true }) + }, + } +} diff --git a/packages/shared/test/kilocode/filesystem-containment.test.ts b/packages/core/test/kilocode/filesystem-containment.test.ts similarity index 87% rename from packages/shared/test/kilocode/filesystem-containment.test.ts rename to packages/core/test/kilocode/filesystem-containment.test.ts index 9f98b926002..fdf0f31720a 100644 --- a/packages/shared/test/kilocode/filesystem-containment.test.ts +++ b/packages/core/test/kilocode/filesystem-containment.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" describe("kilocode filesystem containment", () => { test("keeps dot-prefixed child names internal", () => { diff --git a/packages/shared/test/lib/effect.ts b/packages/core/test/lib/effect.ts similarity index 100% rename from packages/shared/test/lib/effect.ts rename to packages/core/test/lib/effect.ts diff --git a/packages/core/test/npm-config.test.ts b/packages/core/test/npm-config.test.ts new file mode 100644 index 00000000000..895b35db540 --- /dev/null +++ b/packages/core/test/npm-config.test.ts @@ -0,0 +1,51 @@ +import path from "path" +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { NpmConfig } from "@opencode-ai/core/npm-config" +import { tmpdir } from "./fixture/tmpdir" + +describe("NpmConfig.load", () => { + test("reads registry from project .npmrc", async () => { + await using tmp = await tmpdir() + await Bun.write(path.join(tmp.path, ".npmrc"), "registry=https://registry.example.test/\n") + + const config = await Effect.runPromise(NpmConfig.load(tmp.path)) + + expect(config.registry).toBe("https://registry.example.test/") + }) + + test("reads scoped registries from project .npmrc", async () => { + await using tmp = await tmpdir() + await Bun.write(path.join(tmp.path, ".npmrc"), "@acme:registry=https://npm.acme.test/\n") + + const config = await Effect.runPromise(NpmConfig.load(tmp.path)) + + expect(config["@acme:registry"]).toBe("https://npm.acme.test/") + }) + + test("flattens boolean and list options", async () => { + await using tmp = await tmpdir() + await Bun.write(path.join(tmp.path, ".npmrc"), "ignore-scripts=true\nomit[]=dev\nomit[]=optional\n") + + const config = await Effect.runPromise(NpmConfig.load(tmp.path)) + + expect(config.ignoreScripts).toBe(true) + expect(config.omit).toEqual(["dev", "optional"]) + }) +}) + +describe("NpmConfig.registry", () => { + test("normalizes configured registry without trailing slash", async () => { + await using tmp = await tmpdir() + await Bun.write(path.join(tmp.path, ".npmrc"), "registry=https://registry.example.test/\n") + + await expect(Effect.runPromise(NpmConfig.registry(tmp.path))).resolves.toBe("https://registry.example.test") + }) + + test("leaves configured registry without trailing slash unchanged", async () => { + await using tmp = await tmpdir() + await Bun.write(path.join(tmp.path, ".npmrc"), "registry=https://registry.example.test\n") + + await expect(Effect.runPromise(NpmConfig.registry(tmp.path))).resolves.toBe("https://registry.example.test") + }) +}) diff --git a/packages/core/test/npm.test.ts b/packages/core/test/npm.test.ts new file mode 100644 index 00000000000..3e94a08692c --- /dev/null +++ b/packages/core/test/npm.test.ts @@ -0,0 +1,56 @@ +import fs from "fs/promises" +import path from "path" +import { describe, expect, test } from "bun:test" +import { Npm } from "@opencode-ai/core/npm" +import { tmpdir } from "./fixture/tmpdir" + +const win = process.platform === "win32" + +const writePackage = (dir: string, pkg: Record) => + Bun.write( + path.join(dir, "package.json"), + JSON.stringify({ + version: "1.0.0", + ...pkg, + }), + ) + +describe("Npm.sanitize", () => { + test("keeps normal scoped package specs unchanged", () => { + expect(Npm.sanitize("@opencode/acme")).toBe("@opencode/acme") + expect(Npm.sanitize("@opencode/acme@1.0.0")).toBe("@opencode/acme@1.0.0") + expect(Npm.sanitize("prettier")).toBe("prettier") + }) + + test("handles git https specs", () => { + const spec = "acme@git+https://github.com/opencode/acme.git" + const expected = win ? "acme@git+https_//github.com/opencode/acme.git" : spec + expect(Npm.sanitize(spec)).toBe(expected) + }) +}) + +describe("Npm.install", () => { + test("respects omit from project .npmrc", async () => { + await using tmp = await tmpdir() + + await writePackage(tmp.path, { + name: "fixture", + dependencies: { + "prod-pkg": "file:./prod-pkg", + }, + devDependencies: { + "dev-pkg": "file:./dev-pkg", + }, + }) + await Bun.write(path.join(tmp.path, ".npmrc"), "omit=dev\n") + await fs.mkdir(path.join(tmp.path, "prod-pkg")) + await fs.mkdir(path.join(tmp.path, "dev-pkg")) + await writePackage(path.join(tmp.path, "prod-pkg"), { name: "prod-pkg" }) + await writePackage(path.join(tmp.path, "dev-pkg"), { name: "dev-pkg" }) + + await Npm.install(tmp.path) + + await expect(fs.stat(path.join(tmp.path, "node_modules", "prod-pkg"))).resolves.toBeDefined() + await expect(fs.stat(path.join(tmp.path, "node_modules", "dev-pkg"))).rejects.toThrow() + }) +}) diff --git a/packages/shared/test/util/effect-flock.test.ts b/packages/core/test/util/effect-flock.test.ts similarity index 98% rename from packages/shared/test/util/effect-flock.test.ts rename to packages/core/test/util/effect-flock.test.ts index bd71e4f0229..9e8bc24ace2 100644 --- a/packages/shared/test/util/effect-flock.test.ts +++ b/packages/core/test/util/effect-flock.test.ts @@ -5,10 +5,10 @@ import path from "path" import os from "os" import { Cause, Effect, Exit, Layer } from "effect" import { testEffect } from "../lib/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" -import { Global } from "@opencode-ai/shared/global" -import { Hash } from "@opencode-ai/shared/util/hash" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { EffectFlock } from "@opencode-ai/core/util/effect-flock" +import { Global } from "@opencode-ai/core/global" +import { Hash } from "@opencode-ai/core/util/hash" function lock(dir: string, key: string) { return path.join(dir, Hash.fast(key) + ".lock") diff --git a/packages/shared/test/util/flock.test.ts b/packages/core/test/util/flock.test.ts similarity index 98% rename from packages/shared/test/util/flock.test.ts rename to packages/core/test/util/flock.test.ts index f1053dfd2b0..e1b647b6480 100644 --- a/packages/shared/test/util/flock.test.ts +++ b/packages/core/test/util/flock.test.ts @@ -3,8 +3,8 @@ import fs from "fs/promises" import { spawn } from "child_process" import path from "path" import os from "os" -import { Flock } from "@opencode-ai/shared/util/flock" -import { Hash } from "@opencode-ai/shared/util/hash" +import { Flock } from "@opencode-ai/core/util/flock" +import { Hash } from "@opencode-ai/core/util/hash" type Msg = { key: string diff --git a/packages/shared/tsconfig.json b/packages/core/tsconfig.json similarity index 100% rename from packages/shared/tsconfig.json rename to packages/core/tsconfig.json diff --git a/packages/desktop-electron/.gitignore b/packages/desktop-electron/.gitignore deleted file mode 100644 index ac9d8db9694..00000000000 --- a/packages/desktop-electron/.gitignore +++ /dev/null @@ -1,28 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? -out/ - -resources/opencode-cli* -resources/icons diff --git a/packages/desktop-electron/AGENTS.md b/packages/desktop-electron/AGENTS.md deleted file mode 100644 index 7805ea835f5..00000000000 --- a/packages/desktop-electron/AGENTS.md +++ /dev/null @@ -1,4 +0,0 @@ -# Desktop package notes - -- Renderer process should only call `window.api` from `src/preload`. -- Main process should register IPC handlers in `src/main/ipc.ts`. diff --git a/packages/desktop-electron/README.md b/packages/desktop-electron/README.md deleted file mode 100644 index ebaf4882231..00000000000 --- a/packages/desktop-electron/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# OpenCode Desktop - -Native OpenCode desktop app, built with Tauri v2. - -## Development - -From the repo root: - -```bash -bun install -bun run --cwd packages/desktop tauri dev -``` - -This starts the Vite dev server on http://localhost:1420 and opens the native window. - -If you only want the web dev server (no native shell): - -```bash -bun run --cwd packages/desktop dev -``` - -## Build - -To create a production `dist/` and build the native app bundle: - -```bash -bun run --cwd packages/desktop tauri build -``` - -## Prerequisites - -Running the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions. diff --git a/packages/desktop-electron/electron-builder.config.ts b/packages/desktop-electron/electron-builder.config.ts deleted file mode 100644 index 5eff37547fe..00000000000 --- a/packages/desktop-electron/electron-builder.config.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { execFile } from "node:child_process" -import path from "node:path" -import { fileURLToPath } from "node:url" -import { promisify } from "node:util" - -import type { Configuration } from "electron-builder" - -const execFileAsync = promisify(execFile) -const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..") -const signScript = path.join(rootDir, "script", "sign-windows.ps1") - -async function signWindows(configuration: { path: string }) { - if (process.platform !== "win32") return - if (process.env.GITHUB_ACTIONS !== "true") return - - await execFileAsync( - "pwsh", - ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", signScript, configuration.path], - { cwd: rootDir }, - ) -} - -const channel = (() => { - const raw = process.env.KILO_CHANNEL - if (raw === "dev" || raw === "beta" || raw === "prod") return raw - return "dev" -})() - -const getBase = (): Configuration => ({ - artifactName: "opencode-electron-${os}-${arch}.${ext}", - directories: { - output: "dist", - buildResources: "resources", - }, - files: ["out/**/*", "resources/**/*"], - extraResources: [ - { - from: "native/", - to: "native/", - filter: ["index.js", "index.d.ts", "build/Release/mac_window.node", "swift-build/**"], - }, - ], - mac: { - category: "public.app-category.developer-tools", - icon: `resources/icons/icon.icns`, - hardenedRuntime: true, - gatekeeperAssess: false, - entitlements: "resources/entitlements.plist", - entitlementsInherit: "resources/entitlements.plist", - notarize: true, - target: ["dmg", "zip"], - }, - dmg: { - sign: true, - }, - protocols: { - name: "OpenCode", - schemes: ["opencode"], - }, - win: { - icon: `resources/icons/icon.ico`, - signtoolOptions: { - sign: signWindows, - }, - target: ["nsis"], - }, - nsis: { - oneClick: false, - allowToChangeInstallationDirectory: true, - installerIcon: `resources/icons/icon.ico`, - installerHeaderIcon: `resources/icons/icon.ico`, - }, - linux: { - icon: `resources/icons`, - category: "Development", - target: ["AppImage", "deb", "rpm"], - }, -}) - -function getConfig() { - const base = getBase() - - switch (channel) { - case "dev": { - return { - ...base, - appId: "ai.opencode.desktop.dev", - productName: "OpenCode Dev", - rpm: { packageName: "opencode-dev" }, - } - } - case "beta": { - return { - ...base, - appId: "ai.opencode.desktop.beta", - productName: "OpenCode Beta", - protocols: { name: "OpenCode Beta", schemes: ["opencode"] }, - publish: { provider: "github", owner: "anomalyco", repo: "opencode-beta", channel: "latest" }, - rpm: { packageName: "opencode-beta" }, - } - } - case "prod": { - return { - ...base, - appId: "ai.opencode.desktop", - productName: "OpenCode", - protocols: { name: "OpenCode", schemes: ["opencode"] }, - publish: { provider: "github", owner: "anomalyco", repo: "opencode", channel: "latest" }, - rpm: { packageName: "opencode" }, - } - } - } -} - -export default getConfig() diff --git a/packages/desktop-electron/electron.vite.config.ts b/packages/desktop-electron/electron.vite.config.ts deleted file mode 100644 index 22eedeada64..00000000000 --- a/packages/desktop-electron/electron.vite.config.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { defineConfig } from "electron-vite" -import appPlugin from "@opencode-ai/app/vite" -import * as fs from "node:fs/promises" - -const channel = (() => { - const raw = process.env.KILO_CHANNEL - if (raw === "dev" || raw === "beta" || raw === "prod") return raw - return "dev" -})() - -const KILO_SERVER_DIST = "../opencode/dist/node" - -const nodePtyPkg = `@lydell/node-pty-${process.platform}-${process.arch}` - -export default defineConfig({ - main: { - define: { - "import.meta.env.KILO_CHANNEL": JSON.stringify(channel), - }, - build: { - rollupOptions: { - input: { index: "src/main/index.ts" }, - }, - externalizeDeps: { include: [nodePtyPkg] }, - }, - plugins: [ - { - name: "opencode:node-pty-narrower", - enforce: "pre", - resolveId(s) { - if (s === "@lydell/node-pty") return nodePtyPkg - }, - }, - { - name: "opencode:virtual-server-module", - enforce: "pre", - resolveId(id) { - if (id === "virtual:opencode-server") return this.resolve(`${KILO_SERVER_DIST}/node.js`) - }, - }, - { - name: "opencode:copy-server-assets", - async writeBundle() { - for (const l of await fs.readdir(KILO_SERVER_DIST)) { - if (!l.endsWith(".wasm")) continue - await fs.writeFile(`./out/main/chunks/${l}`, await fs.readFile(`${KILO_SERVER_DIST}/${l}`)) - } - }, - }, - ], - }, - preload: { - build: { - rollupOptions: { - input: { index: "src/preload/index.ts" }, - output: { - format: "cjs", - entryFileNames: "[name].js", - }, - }, - }, - }, - renderer: { - plugins: [appPlugin], - publicDir: "../../../app/public", - root: "src/renderer", - define: { - "import.meta.env.VITE_KILO_CHANNEL": JSON.stringify(channel), - }, - build: { - rollupOptions: { - input: { - main: "src/renderer/index.html", - loading: "src/renderer/loading.html", - }, - }, - }, - }, -}) diff --git a/packages/desktop-electron/icons/README.md b/packages/desktop-electron/icons/README.md deleted file mode 100644 index cf2f8e24c50..00000000000 --- a/packages/desktop-electron/icons/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Tauri Icons - -Here's the process I've been using to create icons: - -- Save source image as `app-icon.png` in `packages/desktop` -- `cd` to `packages/desktop` -- Run `bun tauri icon -o src-tauri/icons/{environment}` -- Use [Image2Icon](https://img2icnsapp.com/)'s 'Big Sur Icon' preset to generate an `icon.icns` file and place it in the appropriate icons folder - -The Image2Icon step is necessary as the `icon.icns` generated by `app-icon.png` does not apply the shadow/padding expected by macOS, -so app icons appear larger than expected. - -For unpackaged Electron on macOS, `app.dock.setIcon()` should use a PNG. Keep `dock.png` in each channel folder synced with the -extracted `icon_128x128@2x.png` from that channel's `icon.icns` so the dev Dock icon matches the packaged app inset. diff --git a/packages/desktop-electron/icons/beta/128x128.png b/packages/desktop-electron/icons/beta/128x128.png deleted file mode 100644 index 751e80f1fda..00000000000 Binary files a/packages/desktop-electron/icons/beta/128x128.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/128x128@2x.png b/packages/desktop-electron/icons/beta/128x128@2x.png deleted file mode 100644 index fe330df419a..00000000000 Binary files a/packages/desktop-electron/icons/beta/128x128@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/32x32.png b/packages/desktop-electron/icons/beta/32x32.png deleted file mode 100644 index 2703048eed1..00000000000 Binary files a/packages/desktop-electron/icons/beta/32x32.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/64x64.png b/packages/desktop-electron/icons/beta/64x64.png deleted file mode 100644 index ecd7fe3142c..00000000000 Binary files a/packages/desktop-electron/icons/beta/64x64.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/Square107x107Logo.png b/packages/desktop-electron/icons/beta/Square107x107Logo.png deleted file mode 100644 index e6ea73f4da2..00000000000 Binary files a/packages/desktop-electron/icons/beta/Square107x107Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/Square142x142Logo.png b/packages/desktop-electron/icons/beta/Square142x142Logo.png deleted file mode 100644 index 74ae729c42b..00000000000 Binary files a/packages/desktop-electron/icons/beta/Square142x142Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/Square150x150Logo.png b/packages/desktop-electron/icons/beta/Square150x150Logo.png deleted file mode 100644 index 0b109b8f4ae..00000000000 Binary files a/packages/desktop-electron/icons/beta/Square150x150Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/Square284x284Logo.png b/packages/desktop-electron/icons/beta/Square284x284Logo.png deleted file mode 100644 index 0261ded42c7..00000000000 Binary files a/packages/desktop-electron/icons/beta/Square284x284Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/Square30x30Logo.png b/packages/desktop-electron/icons/beta/Square30x30Logo.png deleted file mode 100644 index 34158f10a49..00000000000 Binary files a/packages/desktop-electron/icons/beta/Square30x30Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/Square310x310Logo.png b/packages/desktop-electron/icons/beta/Square310x310Logo.png deleted file mode 100644 index f18bfada4cd..00000000000 Binary files a/packages/desktop-electron/icons/beta/Square310x310Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/Square44x44Logo.png b/packages/desktop-electron/icons/beta/Square44x44Logo.png deleted file mode 100644 index 6d1cc06c086..00000000000 Binary files a/packages/desktop-electron/icons/beta/Square44x44Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/Square71x71Logo.png b/packages/desktop-electron/icons/beta/Square71x71Logo.png deleted file mode 100644 index a26084dc2fc..00000000000 Binary files a/packages/desktop-electron/icons/beta/Square71x71Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/Square89x89Logo.png b/packages/desktop-electron/icons/beta/Square89x89Logo.png deleted file mode 100644 index 58b0eb6053c..00000000000 Binary files a/packages/desktop-electron/icons/beta/Square89x89Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/StoreLogo.png b/packages/desktop-electron/icons/beta/StoreLogo.png deleted file mode 100644 index 648fd2114d7..00000000000 Binary files a/packages/desktop-electron/icons/beta/StoreLogo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml b/packages/desktop-electron/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 2ffbf24b689..00000000000 --- a/packages/desktop-electron/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher.png b/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 39d1dd0d519..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 84908e71c1f..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher_round.png b/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index a6b8cb61624..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher.png b/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 6522e0fba8a..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index b3449bd4f3f..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher_round.png b/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 7aa97d82761..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher.png b/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 82bc9d22a69..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index 6b031ce8515..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 34859de5ef0..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher.png b/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 4cdb71d62b6..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index a64be6ada1d..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 2de3c27342a..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher.png b/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 0ead288664d..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index bdd1748258a..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 69f74758ecf..00000000000 Binary files a/packages/desktop-electron/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/android/values/ic_launcher_background.xml b/packages/desktop-electron/icons/beta/android/values/ic_launcher_background.xml deleted file mode 100644 index ea9c223a6cb..00000000000 --- a/packages/desktop-electron/icons/beta/android/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #fff - \ No newline at end of file diff --git a/packages/desktop-electron/icons/beta/dock.png b/packages/desktop-electron/icons/beta/dock.png deleted file mode 100644 index f274ef64598..00000000000 Binary files a/packages/desktop-electron/icons/beta/dock.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/icon.icns b/packages/desktop-electron/icons/beta/icon.icns deleted file mode 100644 index f98de5da88b..00000000000 Binary files a/packages/desktop-electron/icons/beta/icon.icns and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/icon.ico b/packages/desktop-electron/icons/beta/icon.ico deleted file mode 100644 index df8588c8e4c..00000000000 Binary files a/packages/desktop-electron/icons/beta/icon.ico and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/icon.png b/packages/desktop-electron/icons/beta/icon.png deleted file mode 100644 index 5313049562a..00000000000 Binary files a/packages/desktop-electron/icons/beta/icon.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-20x20@1x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-20x20@1x.png deleted file mode 100644 index e8ebb28efe1..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-20x20@1x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-20x20@2x-1.png b/packages/desktop-electron/icons/beta/ios/AppIcon-20x20@2x-1.png deleted file mode 100644 index 50c8015dea4..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-20x20@2x-1.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-20x20@2x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-20x20@2x.png deleted file mode 100644 index 50c8015dea4..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-20x20@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-20x20@3x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-20x20@3x.png deleted file mode 100644 index 6e290dbc689..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-20x20@3x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-29x29@1x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-29x29@1x.png deleted file mode 100644 index 4ef554b4de3..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-29x29@1x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-29x29@2x-1.png b/packages/desktop-electron/icons/beta/ios/AppIcon-29x29@2x-1.png deleted file mode 100644 index b9ddfd47c88..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-29x29@2x-1.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-29x29@2x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-29x29@2x.png deleted file mode 100644 index b9ddfd47c88..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-29x29@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-29x29@3x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-29x29@3x.png deleted file mode 100644 index 052322d6821..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-29x29@3x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-40x40@1x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-40x40@1x.png deleted file mode 100644 index 50c8015dea4..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-40x40@1x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-40x40@2x-1.png b/packages/desktop-electron/icons/beta/ios/AppIcon-40x40@2x-1.png deleted file mode 100644 index 9317b25001c..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-40x40@2x-1.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-40x40@2x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-40x40@2x.png deleted file mode 100644 index 9317b25001c..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-40x40@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-40x40@3x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-40x40@3x.png deleted file mode 100644 index 6b921a17e34..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-40x40@3x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-512@2x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-512@2x.png deleted file mode 100644 index b83131d64bc..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-512@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-60x60@2x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-60x60@2x.png deleted file mode 100644 index 6b921a17e34..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-60x60@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-60x60@3x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-60x60@3x.png deleted file mode 100644 index 685004995cc..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-60x60@3x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-76x76@1x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-76x76@1x.png deleted file mode 100644 index 1ffceb752a5..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-76x76@1x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-76x76@2x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-76x76@2x.png deleted file mode 100644 index 81c4178c912..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-76x76@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/beta/ios/AppIcon-83.5x83.5@2x.png b/packages/desktop-electron/icons/beta/ios/AppIcon-83.5x83.5@2x.png deleted file mode 100644 index d5453adffbd..00000000000 Binary files a/packages/desktop-electron/icons/beta/ios/AppIcon-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/128x128.png b/packages/desktop-electron/icons/dev/128x128.png deleted file mode 100644 index d7fc4db1498..00000000000 Binary files a/packages/desktop-electron/icons/dev/128x128.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/128x128@2x.png b/packages/desktop-electron/icons/dev/128x128@2x.png deleted file mode 100644 index 59188230647..00000000000 Binary files a/packages/desktop-electron/icons/dev/128x128@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/32x32.png b/packages/desktop-electron/icons/dev/32x32.png deleted file mode 100644 index 53925cc4f54..00000000000 Binary files a/packages/desktop-electron/icons/dev/32x32.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/64x64.png b/packages/desktop-electron/icons/dev/64x64.png deleted file mode 100644 index a88ef15c64a..00000000000 Binary files a/packages/desktop-electron/icons/dev/64x64.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/Square107x107Logo.png b/packages/desktop-electron/icons/dev/Square107x107Logo.png deleted file mode 100644 index 0de29ec828c..00000000000 Binary files a/packages/desktop-electron/icons/dev/Square107x107Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/Square142x142Logo.png b/packages/desktop-electron/icons/dev/Square142x142Logo.png deleted file mode 100644 index af62e8e1e9e..00000000000 Binary files a/packages/desktop-electron/icons/dev/Square142x142Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/Square150x150Logo.png b/packages/desktop-electron/icons/dev/Square150x150Logo.png deleted file mode 100644 index 2b19dc39cc0..00000000000 Binary files a/packages/desktop-electron/icons/dev/Square150x150Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/Square284x284Logo.png b/packages/desktop-electron/icons/dev/Square284x284Logo.png deleted file mode 100644 index eda6d9901f3..00000000000 Binary files a/packages/desktop-electron/icons/dev/Square284x284Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/Square30x30Logo.png b/packages/desktop-electron/icons/dev/Square30x30Logo.png deleted file mode 100644 index dad821ba84f..00000000000 Binary files a/packages/desktop-electron/icons/dev/Square30x30Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/Square310x310Logo.png b/packages/desktop-electron/icons/dev/Square310x310Logo.png deleted file mode 100644 index 555b3b19790..00000000000 Binary files a/packages/desktop-electron/icons/dev/Square310x310Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/Square44x44Logo.png b/packages/desktop-electron/icons/dev/Square44x44Logo.png deleted file mode 100644 index 9f8ad001f78..00000000000 Binary files a/packages/desktop-electron/icons/dev/Square44x44Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/Square71x71Logo.png b/packages/desktop-electron/icons/dev/Square71x71Logo.png deleted file mode 100644 index 43feb784882..00000000000 Binary files a/packages/desktop-electron/icons/dev/Square71x71Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/Square89x89Logo.png b/packages/desktop-electron/icons/dev/Square89x89Logo.png deleted file mode 100644 index 628cc597f06..00000000000 Binary files a/packages/desktop-electron/icons/dev/Square89x89Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/StoreLogo.png b/packages/desktop-electron/icons/dev/StoreLogo.png deleted file mode 100644 index 8d3aa53cffd..00000000000 Binary files a/packages/desktop-electron/icons/dev/StoreLogo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-anydpi-v26/ic_launcher.xml b/packages/desktop-electron/icons/dev/android/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 2ffbf24b689..00000000000 --- a/packages/desktop-electron/icons/dev/android/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher.png b/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index b355e37fea6..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index c33f8713bc1..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher_round.png b/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 04e37aa6544..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher.png b/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 98e53cd220a..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 40fe6e37863..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher_round.png b/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 4814f1ddf5b..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher.png b/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 608493283e4..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index 898066a3fc0..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 64035c0f3c4..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher.png b/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index f47691bf428..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index dba6f5635b7..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 764702604e3..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher.png b/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 2e8430a604c..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index db953d128c6..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index d5c9ba6a8d1..00000000000 Binary files a/packages/desktop-electron/icons/dev/android/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/android/values/ic_launcher_background.xml b/packages/desktop-electron/icons/dev/android/values/ic_launcher_background.xml deleted file mode 100644 index ea9c223a6cb..00000000000 --- a/packages/desktop-electron/icons/dev/android/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #fff - \ No newline at end of file diff --git a/packages/desktop-electron/icons/dev/dock.png b/packages/desktop-electron/icons/dev/dock.png deleted file mode 100644 index 4953d5531e7..00000000000 Binary files a/packages/desktop-electron/icons/dev/dock.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/icon.icns b/packages/desktop-electron/icons/dev/icon.icns deleted file mode 100644 index d73a94904ad..00000000000 Binary files a/packages/desktop-electron/icons/dev/icon.icns and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/icon.ico b/packages/desktop-electron/icons/dev/icon.ico deleted file mode 100644 index bec385d9aad..00000000000 Binary files a/packages/desktop-electron/icons/dev/icon.ico and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/icon.png b/packages/desktop-electron/icons/dev/icon.png deleted file mode 100644 index 6de37ea2942..00000000000 Binary files a/packages/desktop-electron/icons/dev/icon.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-20x20@1x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-20x20@1x.png deleted file mode 100644 index 0e823043e76..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-20x20@1x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-20x20@2x-1.png b/packages/desktop-electron/icons/dev/ios/AppIcon-20x20@2x-1.png deleted file mode 100644 index 54e4b2aacab..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-20x20@2x-1.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-20x20@2x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-20x20@2x.png deleted file mode 100644 index 54e4b2aacab..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-20x20@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-20x20@3x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-20x20@3x.png deleted file mode 100644 index 645b01561a1..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-20x20@3x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-29x29@1x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-29x29@1x.png deleted file mode 100644 index 054225c6e9f..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-29x29@1x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-29x29@2x-1.png b/packages/desktop-electron/icons/dev/ios/AppIcon-29x29@2x-1.png deleted file mode 100644 index 0b1b2e0b7fe..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-29x29@2x-1.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-29x29@2x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-29x29@2x.png deleted file mode 100644 index 0b1b2e0b7fe..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-29x29@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-29x29@3x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-29x29@3x.png deleted file mode 100644 index d2c42592b3d..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-29x29@3x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-40x40@1x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-40x40@1x.png deleted file mode 100644 index 54e4b2aacab..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-40x40@1x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-40x40@2x-1.png b/packages/desktop-electron/icons/dev/ios/AppIcon-40x40@2x-1.png deleted file mode 100644 index 471ed2eec34..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-40x40@2x-1.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-40x40@2x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-40x40@2x.png deleted file mode 100644 index 471ed2eec34..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-40x40@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-40x40@3x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-40x40@3x.png deleted file mode 100644 index 1a490cbf16f..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-40x40@3x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-512@2x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-512@2x.png deleted file mode 100644 index f53b404e5f8..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-512@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-60x60@2x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-60x60@2x.png deleted file mode 100644 index 1a490cbf16f..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-60x60@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-60x60@3x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-60x60@3x.png deleted file mode 100644 index bdc759eefe2..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-60x60@3x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-76x76@1x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-76x76@1x.png deleted file mode 100644 index d22096a2dfe..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-76x76@1x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-76x76@2x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-76x76@2x.png deleted file mode 100644 index d675773d17e..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-76x76@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/dev/ios/AppIcon-83.5x83.5@2x.png b/packages/desktop-electron/icons/dev/ios/AppIcon-83.5x83.5@2x.png deleted file mode 100644 index 31698afce20..00000000000 Binary files a/packages/desktop-electron/icons/dev/ios/AppIcon-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/128x128.png b/packages/desktop-electron/icons/prod/128x128.png deleted file mode 100644 index caf7b02eb36..00000000000 Binary files a/packages/desktop-electron/icons/prod/128x128.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/128x128@2x.png b/packages/desktop-electron/icons/prod/128x128@2x.png deleted file mode 100644 index 47fe4c61ea2..00000000000 Binary files a/packages/desktop-electron/icons/prod/128x128@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/32x32.png b/packages/desktop-electron/icons/prod/32x32.png deleted file mode 100644 index 5868bcc9331..00000000000 Binary files a/packages/desktop-electron/icons/prod/32x32.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/64x64.png b/packages/desktop-electron/icons/prod/64x64.png deleted file mode 100644 index 1ed7425d855..00000000000 Binary files a/packages/desktop-electron/icons/prod/64x64.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/Square107x107Logo.png b/packages/desktop-electron/icons/prod/Square107x107Logo.png deleted file mode 100644 index 1db249bf720..00000000000 Binary files a/packages/desktop-electron/icons/prod/Square107x107Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/Square142x142Logo.png b/packages/desktop-electron/icons/prod/Square142x142Logo.png deleted file mode 100644 index 1961c340816..00000000000 Binary files a/packages/desktop-electron/icons/prod/Square142x142Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/Square150x150Logo.png b/packages/desktop-electron/icons/prod/Square150x150Logo.png deleted file mode 100644 index abc507347e6..00000000000 Binary files a/packages/desktop-electron/icons/prod/Square150x150Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/Square284x284Logo.png b/packages/desktop-electron/icons/prod/Square284x284Logo.png deleted file mode 100644 index 51e2a1b9fea..00000000000 Binary files a/packages/desktop-electron/icons/prod/Square284x284Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/Square30x30Logo.png b/packages/desktop-electron/icons/prod/Square30x30Logo.png deleted file mode 100644 index 066a1fd0c87..00000000000 Binary files a/packages/desktop-electron/icons/prod/Square30x30Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/Square310x310Logo.png b/packages/desktop-electron/icons/prod/Square310x310Logo.png deleted file mode 100644 index 2a85c8e9522..00000000000 Binary files a/packages/desktop-electron/icons/prod/Square310x310Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/Square44x44Logo.png b/packages/desktop-electron/icons/prod/Square44x44Logo.png deleted file mode 100644 index c855b80632b..00000000000 Binary files a/packages/desktop-electron/icons/prod/Square44x44Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/Square71x71Logo.png b/packages/desktop-electron/icons/prod/Square71x71Logo.png deleted file mode 100644 index c8168f71119..00000000000 Binary files a/packages/desktop-electron/icons/prod/Square71x71Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/Square89x89Logo.png b/packages/desktop-electron/icons/prod/Square89x89Logo.png deleted file mode 100644 index 19ec1777dec..00000000000 Binary files a/packages/desktop-electron/icons/prod/Square89x89Logo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/StoreLogo.png b/packages/desktop-electron/icons/prod/StoreLogo.png deleted file mode 100644 index 3fd053d349a..00000000000 Binary files a/packages/desktop-electron/icons/prod/StoreLogo.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-anydpi-v26/ic_launcher.xml b/packages/desktop-electron/icons/prod/android/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 2ffbf24b689..00000000000 --- a/packages/desktop-electron/icons/prod/android/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher.png b/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 4f3ea0e3672..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 7db80699bcc..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher_round.png b/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index a54ebe65286..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher.png b/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 9337ccfa3fc..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 0bfc1082e68..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher_round.png b/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 5b02ec732e3..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher.png b/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 322aeaeaaad..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index ca1e336cc34..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index f7111079920..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher.png b/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 287a6b500b6..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 9d3d06a867b..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index d4b6fde1b81..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher.png b/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index bde8d75967a..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 03df7809da9..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher_round.png b/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 62363be0470..00000000000 Binary files a/packages/desktop-electron/icons/prod/android/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/android/values/ic_launcher_background.xml b/packages/desktop-electron/icons/prod/android/values/ic_launcher_background.xml deleted file mode 100644 index ea9c223a6cb..00000000000 --- a/packages/desktop-electron/icons/prod/android/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #fff - \ No newline at end of file diff --git a/packages/desktop-electron/icons/prod/dock.png b/packages/desktop-electron/icons/prod/dock.png deleted file mode 100644 index f2ab694e97d..00000000000 Binary files a/packages/desktop-electron/icons/prod/dock.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/icon.icns b/packages/desktop-electron/icons/prod/icon.icns deleted file mode 100644 index be910ad5f93..00000000000 Binary files a/packages/desktop-electron/icons/prod/icon.icns and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/icon.ico b/packages/desktop-electron/icons/prod/icon.ico deleted file mode 100644 index ff88d21e4c5..00000000000 Binary files a/packages/desktop-electron/icons/prod/icon.ico and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/icon.png b/packages/desktop-electron/icons/prod/icon.png deleted file mode 100644 index 0ecbb6d5f8f..00000000000 Binary files a/packages/desktop-electron/icons/prod/icon.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-20x20@1x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-20x20@1x.png deleted file mode 100644 index eb137e164af..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-20x20@1x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-20x20@2x-1.png b/packages/desktop-electron/icons/prod/ios/AppIcon-20x20@2x-1.png deleted file mode 100644 index aa76ab10bae..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-20x20@2x-1.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-20x20@2x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-20x20@2x.png deleted file mode 100644 index aa76ab10bae..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-20x20@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-20x20@3x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-20x20@3x.png deleted file mode 100644 index c58ea3d49bd..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-20x20@3x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-29x29@1x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-29x29@1x.png deleted file mode 100644 index 0eeb4d9bf9e..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-29x29@1x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-29x29@2x-1.png b/packages/desktop-electron/icons/prod/ios/AppIcon-29x29@2x-1.png deleted file mode 100644 index 32601c70a14..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-29x29@2x-1.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-29x29@2x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-29x29@2x.png deleted file mode 100644 index 32601c70a14..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-29x29@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-29x29@3x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-29x29@3x.png deleted file mode 100644 index a372c4a111f..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-29x29@3x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-40x40@1x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-40x40@1x.png deleted file mode 100644 index aa76ab10bae..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-40x40@1x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-40x40@2x-1.png b/packages/desktop-electron/icons/prod/ios/AppIcon-40x40@2x-1.png deleted file mode 100644 index e82ce2765f1..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-40x40@2x-1.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-40x40@2x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-40x40@2x.png deleted file mode 100644 index e82ce2765f1..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-40x40@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-40x40@3x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-40x40@3x.png deleted file mode 100644 index 15ad5936285..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-40x40@3x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-512@2x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-512@2x.png deleted file mode 100644 index 2260671c001..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-512@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-60x60@2x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-60x60@2x.png deleted file mode 100644 index 15ad5936285..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-60x60@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-60x60@3x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-60x60@3x.png deleted file mode 100644 index 5c66bd3b181..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-60x60@3x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-76x76@1x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-76x76@1x.png deleted file mode 100644 index a5b05f3b50f..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-76x76@1x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-76x76@2x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-76x76@2x.png deleted file mode 100644 index 9c0615d411d..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-76x76@2x.png and /dev/null differ diff --git a/packages/desktop-electron/icons/prod/ios/AppIcon-83.5x83.5@2x.png b/packages/desktop-electron/icons/prod/ios/AppIcon-83.5x83.5@2x.png deleted file mode 100644 index 6b792b36ad3..00000000000 Binary files a/packages/desktop-electron/icons/prod/ios/AppIcon-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json deleted file mode 100644 index 419ac63ae9c..00000000000 --- a/packages/desktop-electron/package.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "name": "@opencode-ai/desktop-electron", - "private": true, - "version": "7.2.33", - "type": "module", - "license": "MIT", - "homepage": "https://opencode.ai", - "author": { - "name": "OpenCode", - "email": "hello@opencode.ai" - }, - "scripts": { - "typecheck": "tsgo -b", - "predev": "bun ./scripts/predev.ts", - "dev": "electron-vite dev", - "prebuild": "bun ./scripts/prebuild.ts", - "build": "electron-vite build", - "preview": "electron-vite preview", - "package": "electron-builder --config electron-builder.config.ts", - "package:mac": "electron-builder --mac --config electron-builder.config.ts", - "package:win": "electron-builder --win --config electron-builder.config.ts", - "package:linux": "electron-builder --linux --config electron-builder.config.ts", - "native:build": "bun install --cwd native" - }, - "main": "./out/main/index.js", - "dependencies": { - "effect": "catalog:", - "electron-context-menu": "4.1.2", - "electron-log": "^5", - "electron-store": "^10", - "electron-updater": "^6", - "electron-window-state": "^5.0.3", - "drizzle-orm": "catalog:", - "marked": "^15", - "@opencode-ai/app": "workspace:*", - "@opencode-ai/ui": "workspace:*", - "@solid-primitives/i18n": "2.2.1", - "@solid-primitives/storage": "catalog:", - "@solidjs/meta": "catalog:", - "@solidjs/router": "0.15.4", - "solid-js": "catalog:", - "tree-kill": "^1.2.2" - }, - "devDependencies": { - "@actions/artifact": "4.0.0", - "@lydell/node-pty": "catalog:", - "@opencode-ai/app": "workspace:*", - "@opencode-ai/ui": "workspace:*", - "@solid-primitives/i18n": "2.2.1", - "@solid-primitives/storage": "catalog:", - "@solidjs/meta": "catalog:", - "@solidjs/router": "0.15.4", - "@types/bun": "catalog:", - "@types/node": "catalog:", - "@typescript/native-preview": "catalog:", - "@valibot/to-json-schema": "1.6.0", - "electron": "41.2.1", - "electron-builder": "^26", - "electron-vite": "^5", - "solid-js": "catalog:", - "sury": "11.0.0-alpha.4", - "typescript": "~5.6.2", - "vite": "catalog:", - "zod-openapi": "5.4.6" - }, - "optionalDependencies": { - "@lydell/node-pty-darwin-arm64": "1.2.0-beta.10", - "@lydell/node-pty-darwin-x64": "1.2.0-beta.10", - "@lydell/node-pty-linux-arm64": "1.2.0-beta.10", - "@lydell/node-pty-linux-x64": "1.2.0-beta.10", - "@lydell/node-pty-win32-arm64": "1.2.0-beta.10", - "@lydell/node-pty-win32-x64": "1.2.0-beta.10" - }, - "peerDependencies": {} -} diff --git a/packages/desktop-electron/resources/entitlements.plist b/packages/desktop-electron/resources/entitlements.plist deleted file mode 100644 index b61dc022280..00000000000 --- a/packages/desktop-electron/resources/entitlements.plist +++ /dev/null @@ -1,18 +0,0 @@ - - - - - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.disable-executable-page-protection - - com.apple.security.cs.allow-dyld-environment-variables - - com.apple.security.cs.disable-library-validation - - com.apple.security.device.audio-input - - - diff --git a/packages/desktop-electron/scripts/copy-bundles.ts b/packages/desktop-electron/scripts/copy-bundles.ts deleted file mode 100644 index 6ef3335eb79..00000000000 --- a/packages/desktop-electron/scripts/copy-bundles.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { $ } from "bun" -import * as path from "node:path" - -import { RUST_TARGET } from "./utils" - -if (!RUST_TARGET) throw new Error("RUST_TARGET not defined") - -const BUNDLE_DIR = "dist" -const BUNDLES_OUT_DIR = path.join(process.cwd(), "dist/bundles") - -await $`mkdir -p ${BUNDLES_OUT_DIR}` -await $`cp -r ${BUNDLE_DIR}/* ${BUNDLES_OUT_DIR}` diff --git a/packages/desktop-electron/scripts/copy-icons.ts b/packages/desktop-electron/scripts/copy-icons.ts deleted file mode 100644 index 400f4275710..00000000000 --- a/packages/desktop-electron/scripts/copy-icons.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { $ } from "bun" -import { resolveChannel } from "./utils" - -const arg = process.argv[2] -const channel = arg === "dev" || arg === "beta" || arg === "prod" ? arg : resolveChannel() - -const src = `./icons/${channel}` -const dest = "resources/icons" - -await $`rm -rf ${dest}` -await $`cp -R ${src} ${dest}` -console.log(`Copied ${channel} icons from ${src} to ${dest}`) diff --git a/packages/desktop-electron/scripts/finalize-latest-yml.ts b/packages/desktop-electron/scripts/finalize-latest-yml.ts deleted file mode 100644 index 7d8ad32a88f..00000000000 --- a/packages/desktop-electron/scripts/finalize-latest-yml.ts +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env bun - -import { $ } from "bun" -import path from "path" - -const dir = process.env.LATEST_YML_DIR! -if (!dir) throw new Error("LATEST_YML_DIR is required") - -const repo = process.env.GH_REPO -if (!repo) throw new Error("GH_REPO is required") - -const version = process.env.KILO_VERSION -if (!version) throw new Error("KILO_VERSION is required") - -type FileEntry = { - url: string - sha512: string - size: number - blockMapSize?: number -} - -type LatestYml = { - version: string - files: FileEntry[] - releaseDate: string -} - -function parse(content: string): LatestYml { - const lines = content.split("\n") - let version = "" - let releaseDate = "" - const files: FileEntry[] = [] - let current: Partial | undefined - - const flush = () => { - if (current?.url && current.sha512 && current.size) files.push(current as FileEntry) - current = undefined - } - - for (const line of lines) { - const indented = line.startsWith(" ") || line.startsWith(" -") - if (line.startsWith("version:")) version = line.slice("version:".length).trim() - else if (line.startsWith("releaseDate:")) - releaseDate = line.slice("releaseDate:".length).trim().replace(/^'|'$/g, "") - else if (line.trim().startsWith("- url:")) { - flush() - current = { url: line.trim().slice("- url:".length).trim() } - } else if (indented && current && line.trim().startsWith("sha512:")) - current.sha512 = line.trim().slice("sha512:".length).trim() - else if (indented && current && line.trim().startsWith("size:")) - current.size = Number(line.trim().slice("size:".length).trim()) - else if (indented && current && line.trim().startsWith("blockMapSize:")) - current.blockMapSize = Number(line.trim().slice("blockMapSize:".length).trim()) - else if (!indented && current) flush() - } - flush() - - return { version, files, releaseDate } -} - -function serialize(data: LatestYml) { - const lines = [`version: ${data.version}`, "files:"] - for (const file of data.files) { - lines.push(` - url: ${file.url}`) - lines.push(` sha512: ${file.sha512}`) - lines.push(` size: ${file.size}`) - if (file.blockMapSize) lines.push(` blockMapSize: ${file.blockMapSize}`) - } - lines.push(`releaseDate: '${data.releaseDate}'`) - return lines.join("\n") + "\n" -} - -async function read(subdir: string, filename: string): Promise { - const file = Bun.file(path.join(dir, subdir, filename)) - if (!(await file.exists())) return undefined - return parse(await file.text()) -} - -const output: Record = {} - -// Windows: merge arm64 + x64 into single file -const winX64 = await read("latest-yml-x86_64-pc-windows-msvc", "latest.yml") -const winArm64 = await read("latest-yml-aarch64-pc-windows-msvc", "latest.yml") -if (winX64 || winArm64) { - const base = winArm64 ?? winX64! - output["latest.yml"] = serialize({ - version: base.version, - files: [...(winArm64?.files ?? []), ...(winX64?.files ?? [])], - releaseDate: base.releaseDate, - }) -} - -// Linux x64: pass through -const linuxX64 = await read("latest-yml-x86_64-unknown-linux-gnu", "latest-linux.yml") -if (linuxX64) output["latest-linux.yml"] = serialize(linuxX64) - -// Linux arm64: pass through -const linuxArm64 = await read("latest-yml-aarch64-unknown-linux-gnu", "latest-linux-arm64.yml") -if (linuxArm64) output["latest-linux-arm64.yml"] = serialize(linuxArm64) - -// macOS: merge arm64 + x64 into single file -const macX64 = await read("latest-yml-x86_64-apple-darwin", "latest-mac.yml") -const macArm64 = await read("latest-yml-aarch64-apple-darwin", "latest-mac.yml") -if (macX64 || macArm64) { - const base = macArm64 ?? macX64! - output["latest-mac.yml"] = serialize({ - version: base.version, - files: [...(macArm64?.files ?? []), ...(macX64?.files ?? [])], - releaseDate: base.releaseDate, - }) -} - -// Upload to release -const tag = `v${version}` -const tmp = process.env.RUNNER_TEMP ?? "/tmp" - -for (const [filename, content] of Object.entries(output)) { - const filepath = path.join(tmp, filename) - await Bun.write(filepath, content) - await $`gh release upload ${tag} ${filepath} --clobber --repo ${repo}` - console.log(`uploaded ${filename}`) -} - -console.log("finalized latest yml files") diff --git a/packages/desktop-electron/scripts/prebuild.ts b/packages/desktop-electron/scripts/prebuild.ts deleted file mode 100644 index 46a2475ea51..00000000000 --- a/packages/desktop-electron/scripts/prebuild.ts +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bun -import { $ } from "bun" - -import { resolveChannel } from "./utils" - -const channel = resolveChannel() -await $`bun ./scripts/copy-icons.ts ${channel}` - -await $`cd ../opencode && bun script/build-node.ts` diff --git a/packages/desktop-electron/scripts/predev.ts b/packages/desktop-electron/scripts/predev.ts deleted file mode 100644 index 644c77556b6..00000000000 --- a/packages/desktop-electron/scripts/predev.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { $ } from "bun" - -await $`bun ./scripts/copy-icons.ts ${process.env.KILO_CHANNEL ?? "dev"}` - -await $`cd ../opencode && bun script/build-node.ts` diff --git a/packages/desktop-electron/scripts/prepare.ts b/packages/desktop-electron/scripts/prepare.ts deleted file mode 100755 index 0dfd5a35cbf..00000000000 --- a/packages/desktop-electron/scripts/prepare.ts +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bun -import { Script } from "@opencode-ai/script" - -await import("./prebuild") - -const pkg = await Bun.file("./package.json").json() -pkg.version = Script.version -await Bun.write("./package.json", JSON.stringify(pkg, null, 2) + "\n") -console.log(`Updated package.json version to ${Script.version}`) diff --git a/packages/desktop-electron/scripts/utils.ts b/packages/desktop-electron/scripts/utils.ts deleted file mode 100644 index c039542110b..00000000000 --- a/packages/desktop-electron/scripts/utils.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { $ } from "bun" - -export type Channel = "dev" | "beta" | "prod" - -export function resolveChannel(): Channel { - const raw = Bun.env.KILO_CHANNEL - if (raw === "dev" || raw === "beta" || raw === "prod") return raw - return "dev" -} - -export const SIDECAR_BINARIES: Array<{ rustTarget: string; ocBinary: string; assetExt: string }> = [ - { - rustTarget: "aarch64-apple-darwin", - ocBinary: "opencode-darwin-arm64", - assetExt: "zip", - }, - { - rustTarget: "x86_64-apple-darwin", - ocBinary: "opencode-darwin-x64-baseline", - assetExt: "zip", - }, - { - rustTarget: "aarch64-pc-windows-msvc", - ocBinary: "opencode-windows-arm64", - assetExt: "zip", - }, - { - rustTarget: "x86_64-pc-windows-msvc", - ocBinary: "opencode-windows-x64-baseline", - assetExt: "zip", - }, - { - rustTarget: "x86_64-unknown-linux-gnu", - ocBinary: "opencode-linux-x64-baseline", - assetExt: "tar.gz", - }, - { - rustTarget: "aarch64-unknown-linux-gnu", - ocBinary: "opencode-linux-arm64", - assetExt: "tar.gz", - }, -] - -export const RUST_TARGET = Bun.env.RUST_TARGET - -function nativeTarget() { - const { platform, arch } = process - if (platform === "darwin") return arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin" - if (platform === "win32") return arch === "arm64" ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc" - if (platform === "linux") return arch === "arm64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu" - throw new Error(`Unsupported platform: ${platform}/${arch}`) -} - -export function getCurrentSidecar(target = RUST_TARGET ?? nativeTarget()) { - const binaryConfig = SIDECAR_BINARIES.find((b) => b.rustTarget === target) - if (!binaryConfig) throw new Error(`Sidecar configuration not available for Rust target '${target}'`) - - return binaryConfig -} - -export async function copyBinaryToSidecarFolder(source: string) { - const dir = `resources` - await $`mkdir -p ${dir}` - const dest = windowsify(`${dir}/opencode-cli`) - await $`cp ${source} ${dest}` - if (process.platform === "win32" && process.env.GITHUB_ACTIONS === "true") { - await $`pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File ../../script/sign-windows.ps1 ${dest}` - } - if (process.platform === "darwin") await $`codesign --force --sign - ${dest}` - - console.log(`Copied ${source} to ${dest}`) -} - -export function windowsify(path: string) { - if (path.endsWith(".exe")) return path - return `${path}${process.platform === "win32" ? ".exe" : ""}` -} diff --git a/packages/desktop-electron/src/main/apps.ts b/packages/desktop-electron/src/main/apps.ts deleted file mode 100644 index 174da94a5d9..00000000000 --- a/packages/desktop-electron/src/main/apps.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { execFileSync } from "node:child_process" -import { existsSync, readFileSync, readdirSync } from "node:fs" -import { dirname, extname, join } from "node:path" - -export function checkAppExists(appName: string): boolean { - if (process.platform === "win32") return true - if (process.platform === "linux") return true - return checkMacosApp(appName) -} - -export function resolveAppPath(appName: string): string | null { - if (process.platform !== "win32") return appName - return resolveWindowsAppPath(appName) -} - -export function wslPath(path: string, mode: "windows" | "linux" | null): string { - if (process.platform !== "win32") return path - - const flag = mode === "windows" ? "-w" : "-u" - try { - if (path.startsWith("~")) { - const suffix = path.slice(1) - const cmd = `wslpath ${flag} "$HOME${suffix.replace(/"/g, '\\"')}"` - const output = execFileSync("wsl", ["-e", "sh", "-lc", cmd]) - return output.toString().trim() - } - - const output = execFileSync("wsl", ["-e", "wslpath", flag, path]) - return output.toString().trim() - } catch (error) { - throw new Error(`Failed to run wslpath: ${String(error)}`, { cause: error }) - } -} - -function checkMacosApp(appName: string) { - const locations = [`/Applications/${appName}.app`, `/System/Applications/${appName}.app`] - - const home = process.env.HOME - if (home) locations.push(`${home}/Applications/${appName}.app`) - - if (locations.some((location) => existsSync(location))) return true - - try { - execFileSync("which", [appName]) - return true - } catch { - return false - } -} - -function resolveWindowsAppPath(appName: string): string | null { - let output: string - try { - output = execFileSync("where", [appName]).toString() - } catch { - return null - } - - const paths = output - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line.length > 0) - - const hasExt = (path: string, ext: string) => extname(path).toLowerCase() === `.${ext}` - - const exe = paths.find((path) => hasExt(path, "exe")) - if (exe) return exe - - const resolveCmd = (path: string) => { - const content = readFileSync(path, "utf8") - for (const token of content.split('"').map((value: string) => value.trim())) { - const lower = token.toLowerCase() - if (!lower.includes(".exe")) continue - - const index = lower.indexOf("%~dp0") - if (index >= 0) { - const base = dirname(path) - const suffix = token.slice(index + 5) - const resolved = suffix - .replace(/\//g, "\\") - .split("\\") - .filter((part: string) => part && part !== ".") - .reduce((current: string, part: string) => { - if (part === "..") return dirname(current) - return join(current, part) - }, base) - - if (existsSync(resolved)) return resolved - } - - if (existsSync(token)) return token - } - - return null - } - - for (const path of paths) { - if (hasExt(path, "cmd") || hasExt(path, "bat")) { - const resolved = resolveCmd(path) - if (resolved) return resolved - } - - if (!extname(path)) { - const cmd = `${path}.cmd` - if (existsSync(cmd)) { - const resolved = resolveCmd(cmd) - if (resolved) return resolved - } - - const bat = `${path}.bat` - if (existsSync(bat)) { - const resolved = resolveCmd(bat) - if (resolved) return resolved - } - } - } - - const key = appName - .split("") - .filter((value: string) => /[a-z0-9]/i.test(value)) - .map((value: string) => value.toLowerCase()) - .join("") - - if (key) { - for (const path of paths) { - const dirs = [dirname(path), dirname(dirname(path)), dirname(dirname(dirname(path)))] - for (const dir of dirs) { - try { - for (const entry of readdirSync(dir)) { - const candidate = join(dir, entry) - if (!hasExt(candidate, "exe")) continue - const stem = entry.replace(/\.exe$/i, "") - const name = stem - .split("") - .filter((value: string) => /[a-z0-9]/i.test(value)) - .map((value: string) => value.toLowerCase()) - .join("") - if (name.includes(key) || key.includes(name)) return candidate - } - } catch { - continue - } - } - } - } - - return paths[0] ?? null -} diff --git a/packages/desktop-electron/src/main/constants.ts b/packages/desktop-electron/src/main/constants.ts deleted file mode 100644 index abb65c63378..00000000000 --- a/packages/desktop-electron/src/main/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { app } from "electron" - -type Channel = "dev" | "beta" | "prod" -const raw = import.meta.env.KILO_CHANNEL -export const CHANNEL: Channel = raw === "dev" || raw === "beta" || raw === "prod" ? raw : "dev" - -export const SETTINGS_STORE = "opencode.settings" -export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl" -export const WSL_ENABLED_KEY = "wslEnabled" -export const UPDATER_ENABLED = app.isPackaged && CHANNEL !== "dev" diff --git a/packages/desktop-electron/src/main/env.d.ts b/packages/desktop-electron/src/main/env.d.ts deleted file mode 100644 index 61690242630..00000000000 --- a/packages/desktop-electron/src/main/env.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -interface ImportMetaEnv { - readonly KILO_CHANNEL: string -} - -interface ImportMeta { - readonly env: ImportMetaEnv -} -declare module "virtual:opencode-server" { - export namespace Server { - export const listen: typeof import("../../../opencode/dist/types/src/node").Server.listen - export type Listener = import("../../../opencode/dist/types/src/node").Server.Listener - } - export namespace Config { - export const get: typeof import("../../../opencode/dist/types/src/node").Config.get - export type Info = import("../../../opencode/dist/types/src/node").Config.Info - } - export namespace Log { - export const init: typeof import("../../../opencode/dist/types/src/node").Log.init - } - export namespace Database { - export const Path: typeof import("../../../opencode/dist/types/src/node").Database.Path - export const Client: typeof import("../../../opencode/dist/types/src/node").Database.Client - } - export namespace JsonMigration { - export type Progress = import("../../../opencode/dist/types/src/node").JsonMigration.Progress - export const run: typeof import("../../../opencode/dist/types/src/node").JsonMigration.run - } - export const bootstrap: typeof import("../../../opencode/dist/types/src/node").bootstrap -} diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts deleted file mode 100644 index f00ba5d5732..00000000000 --- a/packages/desktop-electron/src/main/index.ts +++ /dev/null @@ -1,452 +0,0 @@ -import { randomUUID } from "node:crypto" -import { EventEmitter } from "node:events" -import { existsSync } from "node:fs" -import { createServer } from "node:net" -import { homedir } from "node:os" -import { join } from "node:path" -import type { Event } from "electron" -import { app, BrowserWindow, dialog } from "electron" -import pkg from "electron-updater" - -import contextMenu from "electron-context-menu" -contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false }) - -// on macOS apps run in `/` which can cause issues with ripgrep -try { - process.chdir(homedir()) -} catch {} - -process.env.KILO_DISABLE_EMBEDDED_WEB_UI = "true" - -const APP_NAMES: Record = { - dev: "OpenCode Dev", - beta: "OpenCode Beta", - prod: "OpenCode", -} -const APP_IDS: Record = { - dev: "ai.opencode.desktop.dev", - beta: "ai.opencode.desktop.beta", - prod: "ai.opencode.desktop", -} -const appId = app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev" -app.setName(app.isPackaged ? APP_NAMES[CHANNEL] : "OpenCode Dev") -app.setAppUserModelId(appId) -app.setPath("userData", join(app.getPath("appData"), appId)) -const { autoUpdater } = pkg - -import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types" -import { checkAppExists, resolveAppPath, wslPath } from "./apps" -import { CHANNEL, UPDATER_ENABLED } from "./constants" -import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc" -import { initLogging } from "./logging" -import { parseMarkdown } from "./markdown" -import { createMenu } from "./menu" -import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server" -import { - createLoadingWindow, - createMainWindow, - registerRendererProtocol, - setBackgroundColor, - setDockIcon, -} from "./windows" -import { drizzle } from "drizzle-orm/node-sqlite/driver" -import type { Server } from "virtual:opencode-server" - -const initEmitter = new EventEmitter() -let initStep: InitStep = { phase: "server_waiting" } - -let mainWindow: BrowserWindow | null = null -let server: Server.Listener | null = null -const loadingComplete = defer() - -const pendingDeepLinks: string[] = [] - -const serverReady = defer() -const logger = initLogging() - -logger.log("app starting", { - version: app.getVersion(), - packaged: app.isPackaged, -}) - -setupApp() - -function setupApp() { - ensureLoopbackNoProxy() - app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>") - - if (!app.requestSingleInstanceLock()) { - app.quit() - return - } - - app.on("second-instance", (_event: Event, argv: string[]) => { - const urls = argv.filter((arg: string) => arg.startsWith("opencode://")) - if (urls.length) { - logger.log("deep link received via second-instance", { urls }) - emitDeepLinks(urls) - } - focusMainWindow() - }) - - app.on("open-url", (event: Event, url: string) => { - event.preventDefault() - logger.log("deep link received via open-url", { url }) - emitDeepLinks([url]) - }) - - app.on("before-quit", () => { - killSidecar() - }) - - app.on("will-quit", () => { - killSidecar() - }) - - for (const signal of ["SIGINT", "SIGTERM"] as const) { - process.on(signal, () => { - killSidecar() - app.exit(0) - }) - } - - void app.whenReady().then(async () => { - app.setAsDefaultProtocolClient("opencode") - registerRendererProtocol() - setDockIcon() - setupAutoUpdater() - await initialize() - }) -} - -function emitDeepLinks(urls: string[]) { - if (urls.length === 0) return - pendingDeepLinks.push(...urls) - if (mainWindow) sendDeepLinks(mainWindow, urls) -} - -function focusMainWindow() { - if (!mainWindow) return - mainWindow.show() - mainWindow.focus() -} - -function setInitStep(step: InitStep) { - initStep = step - logger.log("init step", { step }) - initEmitter.emit("step", step) -} - -async function initialize() { - const needsMigration = !sqliteFileExists() - const sqliteDone = needsMigration ? defer() : undefined - let overlay: BrowserWindow | null = null - - const port = await getSidecarPort() - const hostname = "127.0.0.1" - const url = `http://${hostname}:${port}` - const password = randomUUID() - - const loadingTask = (async () => { - logger.log("sidecar connection started", { url }) - - initEmitter.on("sqlite", (progress: SqliteMigrationProgress) => { - setInitStep({ phase: "sqlite_waiting" }) - if (overlay) sendSqliteMigrationProgress(overlay, progress) - if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress) - if (progress.type === "Done") sqliteDone?.resolve() - }) - - if (needsMigration) { - const { Database, JsonMigration } = await import("virtual:opencode-server") - await JsonMigration.run(drizzle({ client: Database.Client().$client }), { - progress: (event: { current: number; total: number }) => { - const percent = Math.round(event.current / event.total) * 100 - initEmitter.emit("sqlite", { type: "InProgress", value: percent }) - }, - }) - initEmitter.emit("sqlite", { type: "Done" }) - - sqliteDone?.resolve() - } - - if (needsMigration) { - await sqliteDone?.promise - } - - logger.log("spawning sidecar", { url }) - const { listener, health } = await spawnLocalServer(hostname, port, password) - server = listener - serverReady.resolve({ - url, - username: "kilo", // kilocode_change - password, - }) - - await Promise.race([ - health.wait, - delay(30_000).then(() => { - throw new Error("Sidecar health check timed out") - }), - ]).catch((error) => { - logger.error("sidecar health check failed", error) - }) - - logger.log("loading task finished") - })() - - if (needsMigration) { - const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)]) - if (show) { - overlay = createLoadingWindow() - await delay(1_000) - } - } - - await loadingTask - setInitStep({ phase: "done" }) - - if (overlay) { - await loadingComplete.promise - } - - mainWindow = createMainWindow() - wireMenu() - - overlay?.close() -} - -function wireMenu() { - if (!mainWindow) return - createMenu({ - trigger: (id) => mainWindow && sendMenuCommand(mainWindow, id), - checkForUpdates: () => { - void checkForUpdates(true) - }, - reload: () => mainWindow?.reload(), - relaunch: () => { - killSidecar() - app.relaunch() - app.exit(0) - }, - }) -} - -registerIpcHandlers({ - killSidecar: () => killSidecar(), - awaitInitialization: async (sendStep) => { - sendStep(initStep) - const listener = (step: InitStep) => sendStep(step) - initEmitter.on("step", listener) - try { - logger.log("awaiting server ready") - const res = await serverReady.promise - logger.log("server ready", { url: res.url }) - return res - } finally { - initEmitter.off("step", listener) - } - }, - getWindowConfig: () => ({ updaterEnabled: UPDATER_ENABLED }), - consumeInitialDeepLinks: () => pendingDeepLinks.splice(0), - getDefaultServerUrl: () => getDefaultServerUrl(), - setDefaultServerUrl: (url) => setDefaultServerUrl(url), - getWslConfig: () => Promise.resolve(getWslConfig()), - setWslConfig: (config: WslConfig) => setWslConfig(config), - getDisplayBackend: async () => null, - setDisplayBackend: async () => undefined, - parseMarkdown: async (markdown) => parseMarkdown(markdown), - checkAppExists: async (appName) => checkAppExists(appName), - wslPath: async (path, mode) => wslPath(path, mode), - resolveAppPath: async (appName) => resolveAppPath(appName), - loadingWindowComplete: () => loadingComplete.resolve(), - runUpdater: async (alertOnFail) => checkForUpdates(alertOnFail), - checkUpdate: async () => checkUpdate(), - installUpdate: async () => installUpdate(), - setBackgroundColor: (color) => setBackgroundColor(color), -}) - -function killSidecar() { - if (!server) return - server.stop() - server = null -} - -function ensureLoopbackNoProxy() { - const loopback = ["127.0.0.1", "localhost", "::1"] - const upsert = (key: string) => { - const items = (process.env[key] ?? "") - .split(",") - .map((value: string) => value.trim()) - .filter((value: string) => Boolean(value)) - - for (const host of loopback) { - if (items.some((value: string) => value.toLowerCase() === host)) continue - items.push(host) - } - - process.env[key] = items.join(",") - } - - upsert("NO_PROXY") - upsert("no_proxy") -} - -async function getSidecarPort() { - const fromEnv = process.env.KILO_PORT - if (fromEnv) { - const parsed = Number.parseInt(fromEnv, 10) - if (!Number.isNaN(parsed)) return parsed - } - - return await new Promise((resolve, reject) => { - const server = createServer() - server.on("error", reject) - server.listen(0, "127.0.0.1", () => { - const address = server.address() - if (typeof address !== "object" || !address) { - server.close() - reject(new Error("Failed to get port")) - return - } - const port = address.port - server.close(() => resolve(port)) - }) - }) -} - -function sqliteFileExists() { - const xdg = process.env.XDG_DATA_HOME - const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".local", "share") - return existsSync(join(base, "opencode", "kilo.db")) -} - -function setupAutoUpdater() { - if (!UPDATER_ENABLED) return - autoUpdater.logger = logger - autoUpdater.channel = "latest" - autoUpdater.allowPrerelease = false - autoUpdater.allowDowngrade = true - autoUpdater.autoDownload = false - autoUpdater.autoInstallOnAppQuit = true - logger.log("auto updater configured", { - channel: autoUpdater.channel, - allowPrerelease: autoUpdater.allowPrerelease, - allowDowngrade: autoUpdater.allowDowngrade, - currentVersion: app.getVersion(), - }) -} - -let downloadedUpdateVersion: string | undefined - -async function checkUpdate() { - if (!UPDATER_ENABLED) return { updateAvailable: false } - if (downloadedUpdateVersion) { - logger.log("returning cached downloaded update", { - version: downloadedUpdateVersion, - }) - return { updateAvailable: true, version: downloadedUpdateVersion } - } - logger.log("checking for updates", { - currentVersion: app.getVersion(), - channel: autoUpdater.channel, - allowPrerelease: autoUpdater.allowPrerelease, - allowDowngrade: autoUpdater.allowDowngrade, - }) - try { - const result = await autoUpdater.checkForUpdates() - const updateInfo = result?.updateInfo - logger.log("update metadata fetched", { - releaseVersion: updateInfo?.version ?? null, - releaseDate: updateInfo?.releaseDate ?? null, - releaseName: updateInfo?.releaseName ?? null, - files: updateInfo?.files?.map((file) => file.url) ?? [], - }) - const version = result?.updateInfo?.version - if (result?.isUpdateAvailable === false || !version) { - logger.log("no update available", { - reason: "provider returned no newer version", - }) - return { updateAvailable: false } - } - logger.log("update available", { version }) - await autoUpdater.downloadUpdate() - logger.log("update download completed", { version }) - downloadedUpdateVersion = version - return { updateAvailable: true, version } - } catch (error) { - logger.error("update check failed", error) - return { updateAvailable: false, failed: true } - } -} - -async function installUpdate() { - if (!downloadedUpdateVersion) { - logger.log("install update skipped", { - reason: "no downloaded update ready", - }) - return - } - logger.log("installing downloaded update", { - version: downloadedUpdateVersion, - }) - killSidecar() - autoUpdater.quitAndInstall() -} - -async function checkForUpdates(alertOnFail: boolean) { - if (!UPDATER_ENABLED) return - logger.log("checkForUpdates invoked", { alertOnFail }) - const result = await checkUpdate() - if (!result.updateAvailable) { - if (result.failed) { - logger.log("no update decision", { reason: "update check failed" }) - if (!alertOnFail) return - await dialog.showMessageBox({ - type: "error", - message: "Update check failed.", - title: "Update Error", - }) - return - } - - logger.log("no update decision", { reason: "already up to date" }) - if (!alertOnFail) return - await dialog.showMessageBox({ - type: "info", - message: "You're up to date.", - title: "No Updates", - }) - return - } - - const response = await dialog.showMessageBox({ - type: "info", - message: `Update ${result.version ?? ""} downloaded. Restart now?`, - title: "Update Ready", - buttons: ["Restart", "Later"], - defaultId: 0, - cancelId: 1, - }) - logger.log("update prompt response", { - version: result.version ?? null, - restartNow: response.response === 0, - }) - if (response.response === 0) { - await installUpdate() - } -} - -function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -function defer() { - let resolve!: (value: T) => void - let reject!: (error: Error) => void - const promise = new Promise((res, rej) => { - resolve = res - reject = rej - }) - return { promise, resolve, reject } -} diff --git a/packages/desktop-electron/src/main/ipc.ts b/packages/desktop-electron/src/main/ipc.ts deleted file mode 100644 index 8dbca8eea17..00000000000 --- a/packages/desktop-electron/src/main/ipc.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { execFile } from "node:child_process" -import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron" -import type { IpcMainEvent, IpcMainInvokeEvent } from "electron" - -import type { - InitStep, - ServerReadyData, - SqliteMigrationProgress, - TitlebarTheme, - WindowConfig, - WslConfig, -} from "../preload/types" -import { getStore } from "./store" -import { setTitlebar } from "./windows" - -const pickerFilters = (ext?: string[]) => { - if (!ext || ext.length === 0) return undefined - return [{ name: "Files", extensions: ext }] -} - -type Deps = { - killSidecar: () => void - awaitInitialization: (sendStep: (step: InitStep) => void) => Promise - getWindowConfig: () => Promise | WindowConfig - consumeInitialDeepLinks: () => Promise | string[] - getDefaultServerUrl: () => Promise | string | null - setDefaultServerUrl: (url: string | null) => Promise | void - getWslConfig: () => Promise - setWslConfig: (config: WslConfig) => Promise | void - getDisplayBackend: () => Promise - setDisplayBackend: (backend: string | null) => Promise | void - parseMarkdown: (markdown: string) => Promise | string - checkAppExists: (appName: string) => Promise | boolean - wslPath: (path: string, mode: "windows" | "linux" | null) => Promise - resolveAppPath: (appName: string) => Promise - loadingWindowComplete: () => void - runUpdater: (alertOnFail: boolean) => Promise | void - checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }> - installUpdate: () => Promise | void - setBackgroundColor: (color: string) => void -} - -export function registerIpcHandlers(deps: Deps) { - ipcMain.handle("kill-sidecar", () => deps.killSidecar()) - ipcMain.handle("await-initialization", (event: IpcMainInvokeEvent) => { - const send = (step: InitStep) => event.sender.send("init-step", step) - return deps.awaitInitialization(send) - }) - ipcMain.handle("get-window-config", () => deps.getWindowConfig()) - ipcMain.handle("consume-initial-deep-links", () => deps.consumeInitialDeepLinks()) - ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl()) - ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) => - deps.setDefaultServerUrl(url), - ) - ipcMain.handle("get-wsl-config", () => deps.getWslConfig()) - ipcMain.handle("set-wsl-config", (_event: IpcMainInvokeEvent, config: WslConfig) => deps.setWslConfig(config)) - ipcMain.handle("get-display-backend", () => deps.getDisplayBackend()) - ipcMain.handle("set-display-backend", (_event: IpcMainInvokeEvent, backend: string | null) => - deps.setDisplayBackend(backend), - ) - ipcMain.handle("parse-markdown", (_event: IpcMainInvokeEvent, markdown: string) => deps.parseMarkdown(markdown)) - ipcMain.handle("check-app-exists", (_event: IpcMainInvokeEvent, appName: string) => deps.checkAppExists(appName)) - ipcMain.handle("wsl-path", (_event: IpcMainInvokeEvent, path: string, mode: "windows" | "linux" | null) => - deps.wslPath(path, mode), - ) - ipcMain.handle("resolve-app-path", (_event: IpcMainInvokeEvent, appName: string) => deps.resolveAppPath(appName)) - ipcMain.on("loading-window-complete", () => deps.loadingWindowComplete()) - ipcMain.handle("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail)) - ipcMain.handle("check-update", () => deps.checkUpdate()) - ipcMain.handle("install-update", () => deps.installUpdate()) - ipcMain.handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color)) - ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => { - const store = getStore(name) - const value = store.get(key) - if (value === undefined || value === null) return null - return typeof value === "string" ? value : JSON.stringify(value) - }) - ipcMain.handle("store-set", (_event: IpcMainInvokeEvent, name: string, key: string, value: string) => { - getStore(name).set(key, value) - }) - ipcMain.handle("store-delete", (_event: IpcMainInvokeEvent, name: string, key: string) => { - getStore(name).delete(key) - }) - ipcMain.handle("store-clear", (_event: IpcMainInvokeEvent, name: string) => { - getStore(name).clear() - }) - ipcMain.handle("store-keys", (_event: IpcMainInvokeEvent, name: string) => { - const store = getStore(name) - return Object.keys(store.store) - }) - ipcMain.handle("store-length", (_event: IpcMainInvokeEvent, name: string) => { - const store = getStore(name) - return Object.keys(store.store).length - }) - - ipcMain.handle( - "open-directory-picker", - async (_event: IpcMainInvokeEvent, opts?: { multiple?: boolean; title?: string; defaultPath?: string }) => { - const result = await dialog.showOpenDialog({ - properties: ["openDirectory", ...(opts?.multiple ? ["multiSelections" as const] : []), "createDirectory"], - title: opts?.title ?? "Choose a folder", - defaultPath: opts?.defaultPath, - }) - if (result.canceled) return null - return opts?.multiple ? result.filePaths : result.filePaths[0] - }, - ) - - ipcMain.handle( - "open-file-picker", - async ( - _event: IpcMainInvokeEvent, - opts?: { multiple?: boolean; title?: string; defaultPath?: string; accept?: string[]; extensions?: string[] }, - ) => { - const result = await dialog.showOpenDialog({ - properties: ["openFile", ...(opts?.multiple ? ["multiSelections" as const] : [])], - title: opts?.title ?? "Choose a file", - defaultPath: opts?.defaultPath, - filters: pickerFilters(opts?.extensions), - }) - if (result.canceled) return null - return opts?.multiple ? result.filePaths : result.filePaths[0] - }, - ) - - ipcMain.handle( - "save-file-picker", - async (_event: IpcMainInvokeEvent, opts?: { title?: string; defaultPath?: string }) => { - const result = await dialog.showSaveDialog({ - title: opts?.title ?? "Save file", - defaultPath: opts?.defaultPath, - }) - if (result.canceled) return null - return result.filePath ?? null - }, - ) - - ipcMain.on("open-link", (_event: IpcMainEvent, url: string) => { - void shell.openExternal(url) - }) - - ipcMain.handle("open-path", async (_event: IpcMainInvokeEvent, path: string, app?: string) => { - if (!app) return shell.openPath(path) - await new Promise((resolve, reject) => { - const [cmd, args] = - process.platform === "darwin" ? (["open", ["-a", app, path]] as const) : ([app, [path]] as const) - execFile(cmd, args, (err) => (err ? reject(err) : resolve())) - }) - }) - - ipcMain.handle("read-clipboard-image", () => { - const image = clipboard.readImage() - if (image.isEmpty()) return null - const buffer = image.toPNG().buffer - const size = image.getSize() - return { buffer, width: size.width, height: size.height } - }) - - ipcMain.on("show-notification", (_event: IpcMainEvent, title: string, body?: string) => { - new Notification({ title, body }).show() - }) - - ipcMain.handle("get-window-count", () => BrowserWindow.getAllWindows().length) - - ipcMain.handle("get-window-focused", (event: IpcMainInvokeEvent) => { - const win = BrowserWindow.fromWebContents(event.sender) - return win?.isFocused() ?? false - }) - - ipcMain.handle("set-window-focus", (event: IpcMainInvokeEvent) => { - const win = BrowserWindow.fromWebContents(event.sender) - win?.focus() - }) - - ipcMain.handle("show-window", (event: IpcMainInvokeEvent) => { - const win = BrowserWindow.fromWebContents(event.sender) - win?.show() - }) - - ipcMain.on("relaunch", () => { - app.relaunch() - app.exit(0) - }) - - ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor()) - ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => event.sender.setZoomFactor(factor)) - ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => { - const win = BrowserWindow.fromWebContents(event.sender) - if (!win) return - setTitlebar(win, theme) - }) -} - -export function sendSqliteMigrationProgress(win: BrowserWindow, progress: SqliteMigrationProgress) { - win.webContents.send("sqlite-migration-progress", progress) -} - -export function sendMenuCommand(win: BrowserWindow, id: string) { - win.webContents.send("menu-command", id) -} - -export function sendDeepLinks(win: BrowserWindow, urls: string[]) { - win.webContents.send("deep-link", urls) -} diff --git a/packages/desktop-electron/src/main/logging.ts b/packages/desktop-electron/src/main/logging.ts deleted file mode 100644 index d315b2d344f..00000000000 --- a/packages/desktop-electron/src/main/logging.ts +++ /dev/null @@ -1,40 +0,0 @@ -import log from "electron-log/main.js" -import { readFileSync, readdirSync, statSync, unlinkSync } from "node:fs" -import { dirname, join } from "node:path" - -const MAX_LOG_AGE_DAYS = 7 -const TAIL_LINES = 1000 - -export function initLogging() { - log.transports.file.maxSize = 5 * 1024 * 1024 - cleanup() - return log -} - -export function tail(): string { - try { - const path = log.transports.file.getFile().path - const contents = readFileSync(path, "utf8") - const lines = contents.split("\n") - return lines.slice(Math.max(0, lines.length - TAIL_LINES)).join("\n") - } catch { - return "" - } -} - -function cleanup() { - const path = log.transports.file.getFile().path - const dir = dirname(path) - const cutoff = Date.now() - MAX_LOG_AGE_DAYS * 24 * 60 * 60 * 1000 - - for (const entry of readdirSync(dir)) { - const file = join(dir, entry) - try { - const info = statSync(file) - if (!info.isFile()) continue - if (info.mtimeMs < cutoff) unlinkSync(file) - } catch { - continue - } - } -} diff --git a/packages/desktop-electron/src/main/markdown.ts b/packages/desktop-electron/src/main/markdown.ts deleted file mode 100644 index b956f487602..00000000000 --- a/packages/desktop-electron/src/main/markdown.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { marked, type Tokens } from "marked" - -const renderer = new marked.Renderer() - -renderer.link = ({ href, title, text }: Tokens.Link) => { - const titleAttr = title ? ` title="${title}"` : "" - return `${text}` -} - -export function parseMarkdown(input: string) { - return marked(input, { - renderer, - breaks: false, - gfm: true, - }) -} diff --git a/packages/desktop-electron/src/main/menu.ts b/packages/desktop-electron/src/main/menu.ts deleted file mode 100644 index 0d9a697fa90..00000000000 --- a/packages/desktop-electron/src/main/menu.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { Menu, shell } from "electron" - -import { UPDATER_ENABLED } from "./constants" -import { createMainWindow } from "./windows" - -type Deps = { - trigger: (id: string) => void - checkForUpdates: () => void - reload: () => void - relaunch: () => void -} - -export function createMenu(deps: Deps) { - if (process.platform !== "darwin") return - - const template: Electron.MenuItemConstructorOptions[] = [ - { - label: "OpenCode", - submenu: [ - { role: "about" }, - { - label: "Check for Updates...", - enabled: UPDATER_ENABLED, - click: () => deps.checkForUpdates(), - }, - { - label: "Reload Webview", - click: () => deps.reload(), - }, - { - label: "Restart", - click: () => deps.relaunch(), - }, - { type: "separator" }, - { role: "hide" }, - { role: "hideOthers" }, - { role: "unhide" }, - { type: "separator" }, - { role: "quit" }, - ], - }, - { - label: "File", - submenu: [ - { label: "New Session", accelerator: "Shift+Cmd+S", click: () => deps.trigger("session.new") }, - { label: "Open Project...", accelerator: "Cmd+O", click: () => deps.trigger("project.open") }, - { - label: "New Window", - accelerator: "Cmd+Shift+N", - click: () => createMainWindow(), - }, - { type: "separator" }, - { role: "close" }, - ], - }, - { - label: "Edit", - submenu: [ - { role: "undo" }, - { role: "redo" }, - { type: "separator" }, - { role: "cut" }, - { role: "copy" }, - { role: "paste" }, - { role: "selectAll" }, - ], - }, - { - label: "View", - submenu: [ - { label: "Toggle Sidebar", accelerator: "Cmd+B", click: () => deps.trigger("sidebar.toggle") }, - { label: "Toggle Terminal", accelerator: "Ctrl+`", click: () => deps.trigger("terminal.toggle") }, - { label: "Toggle File Tree", click: () => deps.trigger("fileTree.toggle") }, - { type: "separator" }, - { role: "reload" }, - { role: "toggleDevTools" }, - { type: "separator" }, - { role: "resetZoom" }, - { role: "zoomIn" }, - { role: "zoomOut" }, - { type: "separator" }, - { role: "togglefullscreen" }, - ], - }, - { - label: "Go", - submenu: [ - { label: "Back", accelerator: "Cmd+[", click: () => deps.trigger("common.goBack") }, - { label: "Forward", accelerator: "Cmd+]", click: () => deps.trigger("common.goForward") }, - { type: "separator" }, - { - label: "Previous Session", - accelerator: "Option+Up", - click: () => deps.trigger("session.previous"), - }, - { - label: "Next Session", - accelerator: "Option+Down", - click: () => deps.trigger("session.next"), - }, - { type: "separator" }, - { - label: "Previous Project", - accelerator: "Cmd+Option+Up", - click: () => deps.trigger("project.previous"), - }, - { - label: "Next Project", - accelerator: "Cmd+Option+Down", - click: () => deps.trigger("project.next"), - }, - ], - }, - { role: "windowMenu" }, - { - label: "Help", - submenu: [ - { label: "OpenCode Documentation", click: () => shell.openExternal("https://opencode.ai/docs") }, - { label: "Support Forum", click: () => shell.openExternal("https://discord.com/invite/opencode") }, - { type: "separator" }, - { type: "separator" }, - { - label: "Share Feedback", - click: () => - shell.openExternal("https://github.com/anomalyco/opencode/issues/new?template=feature_request.yml"), - }, - { - label: "Report a Bug", - click: () => shell.openExternal("https://github.com/anomalyco/opencode/issues/new?template=bug_report.yml"), - }, - ], - }, - ] - - Menu.setApplicationMenu(Menu.buildFromTemplate(template)) -} diff --git a/packages/desktop-electron/src/main/migrate.ts b/packages/desktop-electron/src/main/migrate.ts deleted file mode 100644 index 70e3dc9c750..00000000000 --- a/packages/desktop-electron/src/main/migrate.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { app } from "electron" -import log from "electron-log/main.js" -import { existsSync, readdirSync, readFileSync } from "node:fs" -import { homedir } from "node:os" -import { join } from "node:path" -import { CHANNEL } from "./constants" -import { getStore } from "./store" - -const TAURI_MIGRATED_KEY = "tauriMigrated" - -// Resolve the directory where Tauri stored its .dat files for the given app identifier. -// Mirrors Tauri's AppLocalData / AppData resolution per OS. -function tauriDir(id: string) { - switch (process.platform) { - case "darwin": - return join(homedir(), "Library", "Application Support", id) - case "win32": - return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), id) - default: - return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), id) - } -} - -// The Tauri app identifier changes between dev/beta/prod builds. -const TAURI_APP_IDS: Record = { - dev: "ai.opencode.desktop.dev", - beta: "ai.opencode.desktop.beta", - prod: "ai.opencode.desktop", -} -function tauriAppId() { - return app.isPackaged ? TAURI_APP_IDS[CHANNEL] : "ai.opencode.desktop.dev" -} - -// Migrate a single Tauri .dat file into the corresponding electron-store. -// `opencode.settings.dat` is special: it maps to the `opencode.settings` store -// (the electron-store name without the `.dat` extension). All other .dat files -// keep their full filename as the electron-store name so they match what the -// renderer already passes via IPC (e.g. `"default.dat"`, `"opencode.global.dat"`). -function migrateFile(datPath: string, filename: string) { - let data: Record - try { - data = JSON.parse(readFileSync(datPath, "utf-8")) - } catch (err) { - log.warn("tauri migration: failed to parse", filename, err) - return - } - - // opencode.settings.dat → the electron settings store ("opencode.settings"). - // All other .dat files keep their full filename as the store name so they match - // what the renderer passes via IPC (e.g. "default.dat", "opencode.global.dat"). - const storeName = filename === "opencode.settings.dat" ? "opencode.settings" : filename - const target = getStore(storeName) - const migrated: string[] = [] - const skipped: string[] = [] - - for (const [key, value] of Object.entries(data)) { - // Don't overwrite values the user has already set in the Electron app. - if (target.has(key)) { - skipped.push(key) - continue - } - target.set(key, value) - migrated.push(key) - } - - log.log("tauri migration: migrated", filename, "→", storeName, { migrated, skipped }) -} - -export function migrate() { - if (getStore().get(TAURI_MIGRATED_KEY)) { - log.log("tauri migration: already done, skipping") - return - } - - const dir = tauriDir(tauriAppId()) - log.log("tauri migration: starting", { dir }) - - if (!existsSync(dir)) { - log.log("tauri migration: no tauri data directory found, nothing to migrate") - getStore().set(TAURI_MIGRATED_KEY, true) - return - } - - for (const filename of readdirSync(dir)) { - if (!filename.endsWith(".dat")) continue - migrateFile(join(dir, filename), filename) - } - - log.log("tauri migration: complete") - getStore().set(TAURI_MIGRATED_KEY, true) -} diff --git a/packages/desktop-electron/src/main/server.ts b/packages/desktop-electron/src/main/server.ts deleted file mode 100644 index 8c8f0895e93..00000000000 --- a/packages/desktop-electron/src/main/server.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { app } from "electron" -import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants" -import { getUserShell, loadShellEnv } from "./shell-env" -import { getStore } from "./store" - -export type WslConfig = { enabled: boolean } - -export type HealthCheck = { wait: Promise } - -export function getDefaultServerUrl(): string | null { - const value = getStore().get(DEFAULT_SERVER_URL_KEY) - return typeof value === "string" ? value : null -} - -export function setDefaultServerUrl(url: string | null) { - if (url) { - getStore().set(DEFAULT_SERVER_URL_KEY, url) - return - } - - getStore().delete(DEFAULT_SERVER_URL_KEY) -} - -export function getWslConfig(): WslConfig { - const value = getStore().get(WSL_ENABLED_KEY) - return { enabled: typeof value === "boolean" ? value : false } -} - -export function setWslConfig(config: WslConfig) { - getStore().set(WSL_ENABLED_KEY, config.enabled) -} - -export async function spawnLocalServer(hostname: string, port: number, password: string) { - prepareServerEnv(password) - const { Log, Server } = await import("virtual:opencode-server") - await Log.init({ level: "WARN" }) - const listener = await Server.listen({ - port, - hostname, - username: "opencode", - password, - cors: ["oc://renderer"], - }) - - const wait = (async () => { - const url = `http://${hostname}:${port}` - - const ready = async () => { - while (true) { - await new Promise((resolve) => setTimeout(resolve, 100)) - if (await checkHealth(url, password)) return - } - } - - await ready() - })() - - return { listener, health: { wait } } -} - -function prepareServerEnv(password: string) { - const shell = process.platform === "win32" ? null : getUserShell() - const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {} - const env = { - ...process.env, - ...shellEnv, - KILO_EXPERIMENTAL_ICON_DISCOVERY: "true", - KILO_EXPERIMENTAL_FILEWATCHER: "true", - KILO_CLIENT: "desktop", - KILO_SERVER_USERNAME: "opencode", - KILO_SERVER_PASSWORD: password, - XDG_STATE_HOME: app.getPath("userData"), - } - Object.assign(process.env, env) -} - -export async function checkHealth(url: string, password?: string | null): Promise { - let healthUrl: URL - try { - healthUrl = new URL("/global/health", url) - } catch { - return false - } - - const headers = new Headers() - if (password) { - const auth = Buffer.from(`opencode:${password}`).toString("base64") - headers.set("authorization", `Basic ${auth}`) - } - - try { - const res = await fetch(healthUrl, { - method: "GET", - headers, - signal: AbortSignal.timeout(3000), - }) - return res.ok - } catch { - return false - } -} diff --git a/packages/desktop-electron/src/main/shell-env.test.ts b/packages/desktop-electron/src/main/shell-env.test.ts deleted file mode 100644 index 7499cd12197..00000000000 --- a/packages/desktop-electron/src/main/shell-env.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, test } from "bun:test" - -import { isNushell, mergeShellEnv, parseShellEnv } from "./shell-env" - -describe("shell env", () => { - test("parseShellEnv supports null-delimited pairs", () => { - const env = parseShellEnv(Buffer.from("PATH=/usr/bin:/bin\0FOO=bar=baz\0\0")) - - expect(env.PATH).toBe("/usr/bin:/bin") - expect(env.FOO).toBe("bar=baz") - }) - - test("parseShellEnv ignores invalid entries", () => { - const env = parseShellEnv(Buffer.from("INVALID\0=empty\0OK=1\0")) - - expect(Object.keys(env).length).toBe(1) - expect(env.OK).toBe("1") - }) - - test("mergeShellEnv keeps explicit overrides", () => { - const env = mergeShellEnv( - { - PATH: "/shell/path", - HOME: "/tmp/home", - }, - { - PATH: "/desktop/path", - KILO_CLIENT: "desktop", - }, - ) - - expect(env.PATH).toBe("/desktop/path") - expect(env.HOME).toBe("/tmp/home") - expect(env.KILO_CLIENT).toBe("desktop") - }) - - test("isNushell handles path and binary name", () => { - expect(isNushell("nu")).toBe(true) - expect(isNushell("/opt/homebrew/bin/nu")).toBe(true) - expect(isNushell("C:\\Program Files\\nu.exe")).toBe(true) - expect(isNushell("/bin/zsh")).toBe(false) - }) -}) diff --git a/packages/desktop-electron/src/main/shell-env.ts b/packages/desktop-electron/src/main/shell-env.ts deleted file mode 100644 index f57677323c5..00000000000 --- a/packages/desktop-electron/src/main/shell-env.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { spawnSync } from "node:child_process" -import { basename } from "node:path" - -const TIMEOUT = 5_000 - -type Probe = { type: "Loaded"; value: Record } | { type: "Timeout" } | { type: "Unavailable" } - -export function getUserShell() { - return process.env.SHELL || "/bin/sh" -} - -export function parseShellEnv(out: Buffer) { - const env: Record = {} - for (const line of out.toString("utf8").split("\0")) { - if (!line) continue - const ix = line.indexOf("=") - if (ix <= 0) continue - env[line.slice(0, ix)] = line.slice(ix + 1) - } - return env -} - -function probe(shell: string, mode: "-il" | "-l"): Probe { - const out = spawnSync(shell, [mode, "-c", "env -0"], { - stdio: ["ignore", "pipe", "ignore"], - timeout: TIMEOUT, - windowsHide: true, - }) - - const err = out.error as NodeJS.ErrnoException | undefined - if (err) { - if (err.code === "ETIMEDOUT") return { type: "Timeout" } - console.log(`[server] Shell env probe failed for ${shell} ${mode}: ${err.message}`) - return { type: "Unavailable" } - } - - if (out.status !== 0) { - console.log(`[server] Shell env probe exited with non-zero status for ${shell} ${mode}`) - return { type: "Unavailable" } - } - - const env = parseShellEnv(out.stdout) - if (Object.keys(env).length === 0) { - console.log(`[server] Shell env probe returned empty env for ${shell} ${mode}`) - return { type: "Unavailable" } - } - - return { type: "Loaded", value: env } -} - -export function isNushell(shell: string) { - const name = basename(shell).toLowerCase() - const raw = shell.toLowerCase() - return name === "nu" || name === "nu.exe" || raw.endsWith("\\nu.exe") -} - -export function loadShellEnv(shell: string) { - if (isNushell(shell)) { - console.log(`[server] Skipping shell env probe for nushell: ${shell}`) - return null - } - - const interactive = probe(shell, "-il") - if (interactive.type === "Loaded") { - console.log(`[server] Loaded shell environment with -il (${Object.keys(interactive.value).length} vars)`) - return interactive.value - } - if (interactive.type === "Timeout") { - console.warn(`[server] Interactive shell env probe timed out: ${shell}`) - return null - } - - const login = probe(shell, "-l") - if (login.type === "Loaded") { - console.log(`[server] Loaded shell environment with -l (${Object.keys(login.value).length} vars)`) - return login.value - } - - console.warn(`[server] Falling back to app environment: ${shell}`) - return null -} - -export function mergeShellEnv(shell: Record | null, env: Record) { - return { - ...shell, - ...env, - } -} diff --git a/packages/desktop-electron/src/main/store.ts b/packages/desktop-electron/src/main/store.ts deleted file mode 100644 index 61f0c0a4938..00000000000 --- a/packages/desktop-electron/src/main/store.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Store from "electron-store" - -import { SETTINGS_STORE } from "./constants" - -const cache = new Map() - -// We cannot instantiate the electron-store at module load time because -// module import hoisting causes this to run before app.setPath("userData", ...) -// in index.ts has executed, which would result in files being written to the default directory -// (e.g. bad: %APPDATA%\@opencode-ai\desktop-electron\opencode.settings vs good: %APPDATA%\ai.opencode.desktop.dev\opencode.settings). -export function getStore(name = SETTINGS_STORE) { - const cached = cache.get(name) - if (cached) return cached - const next = new Store({ name, fileExtension: "", accessPropertiesByDotNotation: false }) - cache.set(name, next) - return next -} diff --git a/packages/desktop-electron/src/main/windows.ts b/packages/desktop-electron/src/main/windows.ts deleted file mode 100644 index 337e1ca0bcc..00000000000 --- a/packages/desktop-electron/src/main/windows.ts +++ /dev/null @@ -1,206 +0,0 @@ -import windowState from "electron-window-state" -import { app, BrowserWindow, net, nativeImage, nativeTheme, protocol } from "electron" -import { dirname, isAbsolute, join, relative, resolve } from "node:path" -import { fileURLToPath, pathToFileURL } from "node:url" -import type { TitlebarTheme } from "../preload/types" - -const root = dirname(fileURLToPath(import.meta.url)) -const rendererRoot = join(root, "../renderer") -const rendererProtocol = "oc" -const rendererHost = "renderer" - -protocol.registerSchemesAsPrivileged([ - { - scheme: rendererProtocol, - privileges: { - secure: true, - standard: true, - supportFetchAPI: true, - }, - }, -]) - -let backgroundColor: string | undefined - -export function setBackgroundColor(color: string) { - backgroundColor = color -} - -export function getBackgroundColor(): string | undefined { - return backgroundColor -} - -function iconsDir() { - return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons") -} - -function iconPath() { - const ext = process.platform === "win32" ? "ico" : "png" - return join(iconsDir(), `icon.${ext}`) -} - -function tone() { - return nativeTheme.shouldUseDarkColors ? "dark" : "light" -} - -function overlay(theme: Partial = {}) { - const mode = theme.mode ?? tone() - return { - color: "#00000000", - symbolColor: mode === "dark" ? "white" : "black", - height: 40, - } -} - -export function setTitlebar(win: BrowserWindow, theme: Partial = {}) { - if (process.platform !== "win32") return - win.setTitleBarOverlay(overlay(theme)) -} - -export function setDockIcon() { - if (process.platform !== "darwin") return - const icon = nativeImage.createFromPath(join(iconsDir(), "dock.png")) - if (!icon.isEmpty()) app.dock?.setIcon(icon) -} - -export function createMainWindow() { - const state = windowState({ - defaultWidth: 1280, - defaultHeight: 800, - }) - - const mode = tone() - const win = new BrowserWindow({ - x: state.x, - y: state.y, - width: state.width, - height: state.height, - show: false, - title: "OpenCode", - icon: iconPath(), - backgroundColor, - ...(process.platform === "darwin" - ? { - titleBarStyle: "hidden" as const, - trafficLightPosition: { x: 12, y: 14 }, - } - : {}), - ...(process.platform === "win32" - ? { - frame: false, - titleBarStyle: "hidden" as const, - titleBarOverlay: overlay({ mode }), - } - : {}), - webPreferences: { - preload: join(root, "../preload/index.js"), - contextIsolation: true, - nodeIntegration: false, - sandbox: true, - }, - }) - - win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => { - const { requestHeaders } = details - upsertKeyValue(requestHeaders, "Access-Control-Allow-Origin", ["*"]) - callback({ requestHeaders }) - }) - - win.webContents.session.webRequest.onHeadersReceived((details, callback) => { - const { responseHeaders = {} } = details - upsertKeyValue(responseHeaders, "Access-Control-Allow-Origin", ["*"]) - upsertKeyValue(responseHeaders, "Access-Control-Allow-Headers", ["*"]) - callback({ responseHeaders }) - }) - - state.manage(win) - loadWindow(win, "index.html") - wireZoom(win) - - win.once("ready-to-show", () => { - win.show() - }) - - return win -} - -export function createLoadingWindow() { - const mode = tone() - const win = new BrowserWindow({ - width: 640, - height: 480, - resizable: false, - center: true, - show: true, - icon: iconPath(), - backgroundColor, - ...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}), - ...(process.platform === "win32" - ? { - frame: false, - titleBarStyle: "hidden" as const, - titleBarOverlay: overlay({ mode }), - } - : {}), - webPreferences: { - preload: join(root, "../preload/index.js"), - contextIsolation: true, - nodeIntegration: false, - sandbox: true, - }, - }) - - loadWindow(win, "loading.html") - - return win -} - -export function registerRendererProtocol() { - if (protocol.isProtocolHandled(rendererProtocol)) return - - protocol.handle(rendererProtocol, (request) => { - const url = new URL(request.url) - if (url.host !== rendererHost) { - return new Response("Not found", { status: 404 }) - } - - const file = resolve(rendererRoot, `.${decodeURIComponent(url.pathname)}`) - const rel = relative(rendererRoot, file) - if (rel.startsWith("..") || isAbsolute(rel)) { - return new Response("Not found", { status: 404 }) - } - - return net.fetch(pathToFileURL(file).toString()) - }) -} - -function loadWindow(win: BrowserWindow, html: string) { - const devUrl = process.env.ELECTRON_RENDERER_URL - if (devUrl) { - const url = new URL(html, devUrl) - void win.loadURL(url.toString()) - return - } - - void win.loadURL(`${rendererProtocol}://${rendererHost}/${html}`) -} -function wireZoom(win: BrowserWindow) { - win.webContents.setZoomFactor(1) - win.webContents.on("zoom-changed", () => { - win.webContents.setZoomFactor(1) - }) -} - -function upsertKeyValue(obj: Record, keyToChange: string, value: any) { - const keyToChangeLower = keyToChange.toLowerCase() - for (const key of Object.keys(obj)) { - if (key.toLowerCase() === keyToChangeLower) { - // Reassign old key - obj[key] = value - // Done - return - } - } - // Insert at end instead - obj[keyToChange] = value -} diff --git a/packages/desktop-electron/src/preload/index.ts b/packages/desktop-electron/src/preload/index.ts deleted file mode 100644 index 6261419ca55..00000000000 --- a/packages/desktop-electron/src/preload/index.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { contextBridge, ipcRenderer } from "electron" -import type { ElectronAPI, InitStep, SqliteMigrationProgress } from "./types" - -const api: ElectronAPI = { - killSidecar: () => ipcRenderer.invoke("kill-sidecar"), - installCli: () => ipcRenderer.invoke("install-cli"), - awaitInitialization: (onStep) => { - const handler = (_: unknown, step: InitStep) => onStep(step) - ipcRenderer.on("init-step", handler) - return ipcRenderer.invoke("await-initialization").finally(() => { - ipcRenderer.removeListener("init-step", handler) - }) - }, - getWindowConfig: () => ipcRenderer.invoke("get-window-config"), - consumeInitialDeepLinks: () => ipcRenderer.invoke("consume-initial-deep-links"), - getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"), - setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url), - getWslConfig: () => ipcRenderer.invoke("get-wsl-config"), - setWslConfig: (config) => ipcRenderer.invoke("set-wsl-config", config), - getDisplayBackend: () => ipcRenderer.invoke("get-display-backend"), - setDisplayBackend: (backend) => ipcRenderer.invoke("set-display-backend", backend), - parseMarkdownCommand: (markdown) => ipcRenderer.invoke("parse-markdown", markdown), - checkAppExists: (appName) => ipcRenderer.invoke("check-app-exists", appName), - wslPath: (path, mode) => ipcRenderer.invoke("wsl-path", path, mode), - resolveAppPath: (appName) => ipcRenderer.invoke("resolve-app-path", appName), - storeGet: (name, key) => ipcRenderer.invoke("store-get", name, key), - storeSet: (name, key, value) => ipcRenderer.invoke("store-set", name, key, value), - storeDelete: (name, key) => ipcRenderer.invoke("store-delete", name, key), - storeClear: (name) => ipcRenderer.invoke("store-clear", name), - storeKeys: (name) => ipcRenderer.invoke("store-keys", name), - storeLength: (name) => ipcRenderer.invoke("store-length", name), - - getWindowCount: () => ipcRenderer.invoke("get-window-count"), - onSqliteMigrationProgress: (cb) => { - const handler = (_: unknown, progress: SqliteMigrationProgress) => cb(progress) - ipcRenderer.on("sqlite-migration-progress", handler) - return () => ipcRenderer.removeListener("sqlite-migration-progress", handler) - }, - onMenuCommand: (cb) => { - const handler = (_: unknown, id: string) => cb(id) - ipcRenderer.on("menu-command", handler) - return () => ipcRenderer.removeListener("menu-command", handler) - }, - onDeepLink: (cb) => { - const handler = (_: unknown, urls: string[]) => cb(urls) - ipcRenderer.on("deep-link", handler) - return () => ipcRenderer.removeListener("deep-link", handler) - }, - - openDirectoryPicker: (opts) => ipcRenderer.invoke("open-directory-picker", opts), - openFilePicker: (opts) => ipcRenderer.invoke("open-file-picker", opts), - saveFilePicker: (opts) => ipcRenderer.invoke("save-file-picker", opts), - openLink: (url) => ipcRenderer.send("open-link", url), - openPath: (path, app) => ipcRenderer.invoke("open-path", path, app), - readClipboardImage: () => ipcRenderer.invoke("read-clipboard-image"), - showNotification: (title, body) => ipcRenderer.send("show-notification", title, body), - getWindowFocused: () => ipcRenderer.invoke("get-window-focused"), - setWindowFocus: () => ipcRenderer.invoke("set-window-focus"), - showWindow: () => ipcRenderer.invoke("show-window"), - relaunch: () => ipcRenderer.send("relaunch"), - getZoomFactor: () => ipcRenderer.invoke("get-zoom-factor"), - setZoomFactor: (factor) => ipcRenderer.invoke("set-zoom-factor", factor), - setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme), - loadingWindowComplete: () => ipcRenderer.send("loading-window-complete"), - runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail), - checkUpdate: () => ipcRenderer.invoke("check-update"), - installUpdate: () => ipcRenderer.invoke("install-update"), - setBackgroundColor: (color: string) => ipcRenderer.invoke("set-background-color", color), -} - -contextBridge.exposeInMainWorld("api", api) diff --git a/packages/desktop-electron/src/preload/types.ts b/packages/desktop-electron/src/preload/types.ts deleted file mode 100644 index 6e22954d18f..00000000000 --- a/packages/desktop-electron/src/preload/types.ts +++ /dev/null @@ -1,79 +0,0 @@ -export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" } - -export type ServerReadyData = { - url: string - username: string | null - password: string | null -} - -export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" } - -export type WslConfig = { enabled: boolean } - -export type LinuxDisplayBackend = "wayland" | "auto" -export type TitlebarTheme = { - mode: "light" | "dark" -} - -export type WindowConfig = { - updaterEnabled: boolean -} - -export type ElectronAPI = { - killSidecar: () => Promise - installCli: () => Promise - awaitInitialization: (onStep: (step: InitStep) => void) => Promise - getWindowConfig: () => Promise - consumeInitialDeepLinks: () => Promise - getDefaultServerUrl: () => Promise - setDefaultServerUrl: (url: string | null) => Promise - getWslConfig: () => Promise - setWslConfig: (config: WslConfig) => Promise - getDisplayBackend: () => Promise - setDisplayBackend: (backend: LinuxDisplayBackend | null) => Promise - parseMarkdownCommand: (markdown: string) => Promise - checkAppExists: (appName: string) => Promise - wslPath: (path: string, mode: "windows" | "linux" | null) => Promise - resolveAppPath: (appName: string) => Promise - storeGet: (name: string, key: string) => Promise - storeSet: (name: string, key: string, value: string) => Promise - storeDelete: (name: string, key: string) => Promise - storeClear: (name: string) => Promise - storeKeys: (name: string) => Promise - storeLength: (name: string) => Promise - - getWindowCount: () => Promise - onSqliteMigrationProgress: (cb: (progress: SqliteMigrationProgress) => void) => () => void - onMenuCommand: (cb: (id: string) => void) => () => void - onDeepLink: (cb: (urls: string[]) => void) => () => void - - openDirectoryPicker: (opts?: { - multiple?: boolean - title?: string - defaultPath?: string - }) => Promise - openFilePicker: (opts?: { - multiple?: boolean - title?: string - defaultPath?: string - accept?: string[] - extensions?: string[] - }) => Promise - saveFilePicker: (opts?: { title?: string; defaultPath?: string }) => Promise - openLink: (url: string) => void - openPath: (path: string, app?: string) => Promise - readClipboardImage: () => Promise<{ buffer: ArrayBuffer; width: number; height: number } | null> - showNotification: (title: string, body?: string) => void - getWindowFocused: () => Promise - setWindowFocus: () => Promise - showWindow: () => Promise - relaunch: () => void - getZoomFactor: () => Promise - setZoomFactor: (factor: number) => Promise - setTitlebar: (theme: TitlebarTheme) => Promise - loadingWindowComplete: () => void - runUpdater: (alertOnFail: boolean) => Promise - checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }> - installUpdate: () => Promise - setBackgroundColor: (color: string) => Promise -} diff --git a/packages/desktop-electron/src/renderer/cli.ts b/packages/desktop-electron/src/renderer/cli.ts deleted file mode 100644 index 11d3c1f1b08..00000000000 --- a/packages/desktop-electron/src/renderer/cli.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { initI18n, t } from "./i18n" - -export async function installCli(): Promise { - await initI18n() - - try { - const path = await window.api.installCli() - window.alert(t("desktop.cli.installed.message", { path })) - } catch (e) { - window.alert(t("desktop.cli.failed.message", { error: String(e) })) - } -} diff --git a/packages/desktop-electron/src/renderer/env.d.ts b/packages/desktop-electron/src/renderer/env.d.ts deleted file mode 100644 index 6dff3baf1c4..00000000000 --- a/packages/desktop-electron/src/renderer/env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ElectronAPI } from "../preload/types" - -declare global { - interface Window { - api: ElectronAPI - __OPENCODE__?: { - deepLinks?: string[] - } - } -} diff --git a/packages/desktop-electron/src/renderer/html.test.ts b/packages/desktop-electron/src/renderer/html.test.ts deleted file mode 100644 index 1fc5c87178b..00000000000 --- a/packages/desktop-electron/src/renderer/html.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { join, dirname, resolve } from "node:path" -import { existsSync } from "node:fs" -import { fileURLToPath } from "node:url" - -const dir = dirname(fileURLToPath(import.meta.url)) -const root = resolve(dir, "../..") - -const html = async (name: string) => Bun.file(join(dir, name)).text() - -/** - * Packaged Electron windows load renderer HTML via the privileged `oc://` - * protocol. Root-relative asset paths like `src="/foo.js"` would resolve from - * the protocol origin root instead of relative to the current HTML entrypoint. - * - * All local resource references must use relative paths (`./`). - */ -describe("electron renderer html", () => { - for (const name of ["index.html", "loading.html"]) { - describe(name, () => { - test("script src attributes use relative paths", async () => { - const content = await html(name) - const srcs = [...content.matchAll(/\bsrc=["']([^"']+)["']/g)].map((m) => m[1]) - for (const src of srcs) { - expect(src).not.toMatch(/^\/[^/]/) - } - }) - - test("link href attributes use relative paths", async () => { - const content = await html(name) - const hrefs = [...content.matchAll(/]+href=["']([^"']+)["']/g)].map((m) => m[1]) - for (const href of hrefs) { - expect(href).not.toMatch(/^\/[^/]/) - } - }) - - test("no web manifest link (not applicable in Electron)", async () => { - const content = await html(name) - expect(content).not.toContain('rel="manifest"') - }) - }) - } -}) - -/** - * Vite resolves `publicDir` relative to `root`, not the config file. - * This test reads the actual values from electron.vite.config.ts to catch - * regressions where the publicDir path no longer resolves correctly - * after the renderer root is accounted for. - */ -describe("electron vite publicDir", () => { - test("configured publicDir resolves to a directory with oc-theme-preload.js", async () => { - const config = await Bun.file(join(root, "electron.vite.config.ts")).text() - const pub = config.match(/publicDir:\s*["']([^"']+)["']/) - const rendererRoot = config.match(/root:\s*["']([^"']+)["']/) - expect(pub).not.toBeNull() - expect(rendererRoot).not.toBeNull() - const resolved = resolve(root, rendererRoot![1], pub![1]) - expect(existsSync(resolved)).toBe(true) - expect(existsSync(join(resolved, "oc-theme-preload.js"))).toBe(true) - }) -}) diff --git a/packages/desktop-electron/src/renderer/i18n/ar.ts b/packages/desktop-electron/src/renderer/i18n/ar.ts deleted file mode 100644 index fdbf0a80470..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/ar.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "التحقق من وجود تحديثات...", - "desktop.menu.installCli": "تثبيت CLI...", - "desktop.menu.reloadWebview": "إعادة تحميل Webview", - "desktop.menu.restart": "إعادة تشغيل", - - "desktop.dialog.chooseFolder": "اختر مجلدًا", - "desktop.dialog.chooseFile": "اختر ملفًا", - "desktop.dialog.saveFile": "حفظ ملف", - - "desktop.updater.checkFailed.title": "فشل التحقق من التحديثات", - "desktop.updater.checkFailed.message": "فشل التحقق من وجود تحديثات", - "desktop.updater.none.title": "لا توجد تحديثات متاحة", - "desktop.updater.none.message": "أنت تستخدم بالفعل أحدث إصدار من OpenCode", - "desktop.updater.downloadFailed.title": "فشل التحديث", - "desktop.updater.downloadFailed.message": "فشل تنزيل التحديث", - "desktop.updater.downloaded.title": "تم تنزيل التحديث", - "desktop.updater.downloaded.prompt": "تم تنزيل إصدار {{version}} من OpenCode، هل ترغب في تثبيته وإعادة تشغيله؟", - "desktop.updater.installFailed.title": "فشل التحديث", - "desktop.updater.installFailed.message": "فشل تثبيت التحديث", - - "desktop.cli.installed.title": "تم تثبيت CLI", - "desktop.cli.installed.message": "تم تثبيت CLI في {{path}}\n\nأعد تشغيل الطرفية لاستخدام الأمر 'opencode'.", - "desktop.cli.failed.title": "فشل التثبيت", - "desktop.cli.failed.message": "فشل تثبيت CLI: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/br.ts b/packages/desktop-electron/src/renderer/i18n/br.ts deleted file mode 100644 index 75fe2dc32bc..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/br.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Verificar atualizações...", - "desktop.menu.installCli": "Instalar CLI...", - "desktop.menu.reloadWebview": "Recarregar Webview", - "desktop.menu.restart": "Reiniciar", - - "desktop.dialog.chooseFolder": "Escolher uma pasta", - "desktop.dialog.chooseFile": "Escolher um arquivo", - "desktop.dialog.saveFile": "Salvar arquivo", - - "desktop.updater.checkFailed.title": "Falha ao verificar atualizações", - "desktop.updater.checkFailed.message": "Falha ao verificar atualizações", - "desktop.updater.none.title": "Nenhuma atualização disponível", - "desktop.updater.none.message": "Você já está usando a versão mais recente do OpenCode", - "desktop.updater.downloadFailed.title": "Falha na atualização", - "desktop.updater.downloadFailed.message": "Falha ao baixar a atualização", - "desktop.updater.downloaded.title": "Atualização baixada", - "desktop.updater.downloaded.prompt": - "A versão {{version}} do OpenCode foi baixada. Você gostaria de instalá-la e reiniciar?", - "desktop.updater.installFailed.title": "Falha na atualização", - "desktop.updater.installFailed.message": "Falha ao instalar a atualização", - - "desktop.cli.installed.title": "CLI instalada", - "desktop.cli.installed.message": "CLI instalada em {{path}}\n\nReinicie seu terminal para usar o comando 'opencode'.", - "desktop.cli.failed.title": "Falha na instalação", - "desktop.cli.failed.message": "Falha ao instalar a CLI: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/bs.ts b/packages/desktop-electron/src/renderer/i18n/bs.ts deleted file mode 100644 index 58c266f5305..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/bs.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Provjeri ažuriranja...", - "desktop.menu.installCli": "Instaliraj CLI...", - "desktop.menu.reloadWebview": "Ponovo učitavanje webview-a", - "desktop.menu.restart": "Restartuj", - - "desktop.dialog.chooseFolder": "Odaberi folder", - "desktop.dialog.chooseFile": "Odaberi datoteku", - "desktop.dialog.saveFile": "Sačuvaj datoteku", - - "desktop.updater.checkFailed.title": "Provjera ažuriranja nije uspjela", - "desktop.updater.checkFailed.message": "Nije moguće provjeriti ažuriranja", - "desktop.updater.none.title": "Nema dostupnog ažuriranja", - "desktop.updater.none.message": "Već koristiš najnoviju verziju OpenCode-a", - "desktop.updater.downloadFailed.title": "Ažuriranje nije uspjelo", - "desktop.updater.downloadFailed.message": "Neuspjelo preuzimanje ažuriranja", - "desktop.updater.downloaded.title": "Ažuriranje preuzeto", - "desktop.updater.downloaded.prompt": - "Verzija {{version}} OpenCode-a je preuzeta. Želiš li da je instaliraš i ponovo pokreneš aplikaciju?", - "desktop.updater.installFailed.title": "Ažuriranje nije uspjelo", - "desktop.updater.installFailed.message": "Neuspjela instalacija ažuriranja", - - "desktop.cli.installed.title": "CLI instaliran", - "desktop.cli.installed.message": - "CLI je instaliran u {{path}}\n\nRestartuj terminal da bi koristio komandu 'opencode'.", - "desktop.cli.failed.title": "Instalacija nije uspjela", - "desktop.cli.failed.message": "Neuspjela instalacija CLI-a: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/da.ts b/packages/desktop-electron/src/renderer/i18n/da.ts deleted file mode 100644 index 2109495f761..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/da.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Tjek for opdateringer...", - "desktop.menu.installCli": "Installer CLI...", - "desktop.menu.reloadWebview": "Genindlæs Webview", - "desktop.menu.restart": "Genstart", - - "desktop.dialog.chooseFolder": "Vælg en mappe", - "desktop.dialog.chooseFile": "Vælg en fil", - "desktop.dialog.saveFile": "Gem fil", - - "desktop.updater.checkFailed.title": "Opdateringstjek mislykkedes", - "desktop.updater.checkFailed.message": "Kunne ikke tjekke for opdateringer", - "desktop.updater.none.title": "Ingen opdatering tilgængelig", - "desktop.updater.none.message": "Du bruger allerede den nyeste version af OpenCode", - "desktop.updater.downloadFailed.title": "Opdatering mislykkedes", - "desktop.updater.downloadFailed.message": "Kunne ikke downloade opdateringen", - "desktop.updater.downloaded.title": "Opdatering downloadet", - "desktop.updater.downloaded.prompt": - "Version {{version}} af OpenCode er blevet downloadet. Vil du installere den og genstarte?", - "desktop.updater.installFailed.title": "Opdatering mislykkedes", - "desktop.updater.installFailed.message": "Kunne ikke installere opdateringen", - - "desktop.cli.installed.title": "CLI installeret", - "desktop.cli.installed.message": - "CLI installeret i {{path}}\n\nGenstart din terminal for at bruge 'opencode'-kommandoen.", - "desktop.cli.failed.title": "Installation mislykkedes", - "desktop.cli.failed.message": "Kunne ikke installere CLI: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/de.ts b/packages/desktop-electron/src/renderer/i18n/de.ts deleted file mode 100644 index 38ad8096e31..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/de.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Nach Updates suchen...", - "desktop.menu.installCli": "CLI installieren...", - "desktop.menu.reloadWebview": "Webview neu laden", - "desktop.menu.restart": "Neustart", - - "desktop.dialog.chooseFolder": "Ordner auswählen", - "desktop.dialog.chooseFile": "Datei auswählen", - "desktop.dialog.saveFile": "Datei speichern", - - "desktop.updater.checkFailed.title": "Updateprüfung fehlgeschlagen", - "desktop.updater.checkFailed.message": "Updates konnten nicht geprüft werden", - "desktop.updater.none.title": "Kein Update verfügbar", - "desktop.updater.none.message": "Sie verwenden bereits die neueste Version von OpenCode", - "desktop.updater.downloadFailed.title": "Update fehlgeschlagen", - "desktop.updater.downloadFailed.message": "Update konnte nicht heruntergeladen werden", - "desktop.updater.downloaded.title": "Update heruntergeladen", - "desktop.updater.downloaded.prompt": - "Version {{version}} von OpenCode wurde heruntergeladen. Möchten Sie sie installieren und neu starten?", - "desktop.updater.installFailed.title": "Update fehlgeschlagen", - "desktop.updater.installFailed.message": "Update konnte nicht installiert werden", - - "desktop.cli.installed.title": "CLI installiert", - "desktop.cli.installed.message": - "CLI wurde in {{path}} installiert\n\nStarten Sie Ihr Terminal neu, um den Befehl 'opencode' zu verwenden.", - "desktop.cli.failed.title": "Installation fehlgeschlagen", - "desktop.cli.failed.message": "CLI konnte nicht installiert werden: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/en.ts b/packages/desktop-electron/src/renderer/i18n/en.ts deleted file mode 100644 index 4c30380d562..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/en.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Check for Updates...", - "desktop.menu.installCli": "Install CLI...", - "desktop.menu.reloadWebview": "Reload Webview", - "desktop.menu.restart": "Restart", - - "desktop.dialog.chooseFolder": "Choose a folder", - "desktop.dialog.chooseFile": "Choose a file", - "desktop.dialog.saveFile": "Save file", - - "desktop.updater.checkFailed.title": "Update Check Failed", - "desktop.updater.checkFailed.message": "Failed to check for updates", - "desktop.updater.none.title": "No Update Available", - "desktop.updater.none.message": "You are already using the latest version of OpenCode", - "desktop.updater.downloadFailed.title": "Update Failed", - "desktop.updater.downloadFailed.message": "Failed to download update", - "desktop.updater.downloaded.title": "Update Downloaded", - "desktop.updater.downloaded.prompt": - "Version {{version}} of OpenCode has been downloaded, would you like to install it and relaunch?", - "desktop.updater.installFailed.title": "Update Failed", - "desktop.updater.installFailed.message": "Failed to install update", - - "desktop.cli.installed.title": "CLI Installed", - "desktop.cli.installed.message": "CLI installed to {{path}}\n\nRestart your terminal to use the 'opencode' command.", - "desktop.cli.failed.title": "Installation Failed", - "desktop.cli.failed.message": "Failed to install CLI: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/es.ts b/packages/desktop-electron/src/renderer/i18n/es.ts deleted file mode 100644 index 80504a8f248..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/es.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Buscar actualizaciones...", - "desktop.menu.installCli": "Instalar CLI...", - "desktop.menu.reloadWebview": "Recargar Webview", - "desktop.menu.restart": "Reiniciar", - - "desktop.dialog.chooseFolder": "Elegir una carpeta", - "desktop.dialog.chooseFile": "Elegir un archivo", - "desktop.dialog.saveFile": "Guardar archivo", - - "desktop.updater.checkFailed.title": "Comprobación de actualizaciones fallida", - "desktop.updater.checkFailed.message": "No se pudieron buscar actualizaciones", - "desktop.updater.none.title": "No hay actualizaciones disponibles", - "desktop.updater.none.message": "Ya estás usando la versión más reciente de OpenCode", - "desktop.updater.downloadFailed.title": "Actualización fallida", - "desktop.updater.downloadFailed.message": "No se pudo descargar la actualización", - "desktop.updater.downloaded.title": "Actualización descargada", - "desktop.updater.downloaded.prompt": - "Se ha descargado la versión {{version}} de OpenCode. ¿Quieres instalarla y reiniciar?", - "desktop.updater.installFailed.title": "Actualización fallida", - "desktop.updater.installFailed.message": "No se pudo instalar la actualización", - - "desktop.cli.installed.title": "CLI instalada", - "desktop.cli.installed.message": "CLI instalada en {{path}}\n\nReinicia tu terminal para usar el comando 'opencode'.", - "desktop.cli.failed.title": "Instalación fallida", - "desktop.cli.failed.message": "No se pudo instalar la CLI: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/fr.ts b/packages/desktop-electron/src/renderer/i18n/fr.ts deleted file mode 100644 index 4f0bb2b16c6..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/fr.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Vérifier les mises à jour...", - "desktop.menu.installCli": "Installer la CLI...", - "desktop.menu.reloadWebview": "Recharger la Webview", - "desktop.menu.restart": "Redémarrer", - - "desktop.dialog.chooseFolder": "Choisir un dossier", - "desktop.dialog.chooseFile": "Choisir un fichier", - "desktop.dialog.saveFile": "Enregistrer le fichier", - - "desktop.updater.checkFailed.title": "Échec de la vérification des mises à jour", - "desktop.updater.checkFailed.message": "Impossible de vérifier les mises à jour", - "desktop.updater.none.title": "Aucune mise à jour disponible", - "desktop.updater.none.message": "Vous utilisez déjà la dernière version d'OpenCode", - "desktop.updater.downloadFailed.title": "Échec de la mise à jour", - "desktop.updater.downloadFailed.message": "Impossible de télécharger la mise à jour", - "desktop.updater.downloaded.title": "Mise à jour téléchargée", - "desktop.updater.downloaded.prompt": - "La version {{version}} d'OpenCode a été téléchargée. Voulez-vous l'installer et redémarrer ?", - "desktop.updater.installFailed.title": "Échec de la mise à jour", - "desktop.updater.installFailed.message": "Impossible d'installer la mise à jour", - - "desktop.cli.installed.title": "CLI installée", - "desktop.cli.installed.message": - "CLI installée dans {{path}}\n\nRedémarrez votre terminal pour utiliser la commande 'opencode'.", - "desktop.cli.failed.title": "Échec de l'installation", - "desktop.cli.failed.message": "Impossible d'installer la CLI : {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/index.ts b/packages/desktop-electron/src/renderer/i18n/index.ts deleted file mode 100644 index 2a7a7b188e9..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/index.ts +++ /dev/null @@ -1,194 +0,0 @@ -import * as i18n from "@solid-primitives/i18n" - -import { dict as desktopEn } from "./en" -import { dict as desktopZh } from "./zh" -import { dict as desktopZht } from "./zht" -import { dict as desktopKo } from "./ko" -import { dict as desktopDe } from "./de" -import { dict as desktopEs } from "./es" -import { dict as desktopFr } from "./fr" -import { dict as desktopDa } from "./da" -import { dict as desktopJa } from "./ja" -import { dict as desktopPl } from "./pl" -import { dict as desktopRu } from "./ru" -import { dict as desktopAr } from "./ar" -import { dict as desktopNo } from "./no" -import { dict as desktopBr } from "./br" -import { dict as desktopBs } from "./bs" -import { dict as desktopNl } from "./nl" - -import { dict as appEn } from "../../../../app/src/i18n/en" -import { dict as appZh } from "../../../../app/src/i18n/zh" -import { dict as appZht } from "../../../../app/src/i18n/zht" -import { dict as appKo } from "../../../../app/src/i18n/ko" -import { dict as appDe } from "../../../../app/src/i18n/de" -import { dict as appEs } from "../../../../app/src/i18n/es" -import { dict as appFr } from "../../../../app/src/i18n/fr" -import { dict as appDa } from "../../../../app/src/i18n/da" -import { dict as appJa } from "../../../../app/src/i18n/ja" -import { dict as appPl } from "../../../../app/src/i18n/pl" -import { dict as appRu } from "../../../../app/src/i18n/ru" -import { dict as appAr } from "../../../../app/src/i18n/ar" -import { dict as appNo } from "../../../../app/src/i18n/no" -import { dict as appBr } from "../../../../app/src/i18n/br" -import { dict as appBs } from "../../../../app/src/i18n/bs" -import { dict as appNl } from "../../../../app/src/i18n/nl" - -export type Locale = - | "en" - | "zh" - | "zht" - | "ko" - | "de" - | "es" - | "fr" - | "da" - | "ja" - | "pl" - | "ru" - | "ar" - | "no" - | "br" - | "bs" - | "nl" - -type RawDictionary = typeof appEn & typeof desktopEn -type Dictionary = i18n.Flatten - -const LOCALES: readonly Locale[] = [ - "en", - "zh", - "zht", - "ko", - "de", - "es", - "fr", - "da", - "ja", - "pl", - "ru", - "bs", - "nl", - "ar", - "no", - "br", -] - -function detectLocale(): Locale { - if (typeof navigator !== "object") return "en" - - const languages = navigator.languages?.length ? navigator.languages : [navigator.language] - for (const language of languages) { - if (!language) continue - if (language.toLowerCase().startsWith("en")) return "en" - if (language.toLowerCase().startsWith("zh")) { - if (language.toLowerCase().includes("hant")) return "zht" - return "zh" - } - if (language.toLowerCase().startsWith("ko")) return "ko" - if (language.toLowerCase().startsWith("de")) return "de" - if (language.toLowerCase().startsWith("es")) return "es" - if (language.toLowerCase().startsWith("fr")) return "fr" - if (language.toLowerCase().startsWith("da")) return "da" - if (language.toLowerCase().startsWith("ja")) return "ja" - if (language.toLowerCase().startsWith("pl")) return "pl" - if (language.toLowerCase().startsWith("ru")) return "ru" - if (language.toLowerCase().startsWith("ar")) return "ar" - if ( - language.toLowerCase().startsWith("no") || - language.toLowerCase().startsWith("nb") || - language.toLowerCase().startsWith("nn") - ) - return "no" - if (language.toLowerCase().startsWith("pt")) return "br" - if (language.toLowerCase().startsWith("bs")) return "bs" - if (language.toLowerCase().startsWith("nl")) return "nl" - } - - return "en" -} - -function parseLocale(value: unknown): Locale | null { - if (!value) return null - if (typeof value !== "string") return null - if ((LOCALES as readonly string[]).includes(value)) return value as Locale - return null -} - -function parseRecord(value: unknown) { - if (!value || typeof value !== "object") return null - if (Array.isArray(value)) return null - return value as Record -} - -function parseStored(value: unknown) { - if (typeof value !== "string") return value - try { - return JSON.parse(value) as unknown - } catch { - return value - } -} - -function pickLocale(value: unknown): Locale | null { - const direct = parseLocale(value) - if (direct) return direct - - const record = parseRecord(value) - if (!record) return null - - return parseLocale(record.locale) -} - -const base = i18n.flatten({ ...appEn, ...desktopEn }) - -function build(locale: Locale): Dictionary { - if (locale === "en") return base - if (locale === "zh") return { ...base, ...i18n.flatten(appZh), ...i18n.flatten(desktopZh) } - if (locale === "zht") return { ...base, ...i18n.flatten(appZht), ...i18n.flatten(desktopZht) } - if (locale === "de") return { ...base, ...i18n.flatten(appDe), ...i18n.flatten(desktopDe) } - if (locale === "es") return { ...base, ...i18n.flatten(appEs), ...i18n.flatten(desktopEs) } - if (locale === "fr") return { ...base, ...i18n.flatten(appFr), ...i18n.flatten(desktopFr) } - if (locale === "da") return { ...base, ...i18n.flatten(appDa), ...i18n.flatten(desktopDa) } - if (locale === "ja") return { ...base, ...i18n.flatten(appJa), ...i18n.flatten(desktopJa) } - if (locale === "pl") return { ...base, ...i18n.flatten(appPl), ...i18n.flatten(desktopPl) } - if (locale === "ru") return { ...base, ...i18n.flatten(appRu), ...i18n.flatten(desktopRu) } - if (locale === "ar") return { ...base, ...i18n.flatten(appAr), ...i18n.flatten(desktopAr) } - if (locale === "no") return { ...base, ...i18n.flatten(appNo), ...i18n.flatten(desktopNo) } - if (locale === "br") return { ...base, ...i18n.flatten(appBr), ...i18n.flatten(desktopBr) } - if (locale === "bs") return { ...base, ...i18n.flatten(appBs), ...i18n.flatten(desktopBs) } - if (locale === "nl") return { ...base, ...i18n.flatten(appNl), ...i18n.flatten(desktopNl) } - return { ...base, ...i18n.flatten(appKo), ...i18n.flatten(desktopKo) } -} - -const state = { - locale: detectLocale(), - dict: base as Dictionary, - init: undefined as Promise | undefined, -} - -state.dict = build(state.locale) - -const translate = i18n.translator(() => state.dict, i18n.resolveTemplate) - -export function t(key: keyof Dictionary, params?: Record) { - return translate(key, params) -} - -export function initI18n(): Promise { - const cached = state.init - if (cached) return cached - - const promise = (async () => { - const raw = await window.api.storeGet("opencode.global.dat", "language").catch(() => null) - const value = parseStored(raw) - const next = pickLocale(value) ?? state.locale - - state.locale = next - state.dict = build(next) - return next - })().catch(() => state.locale) - - state.init = promise - return promise -} diff --git a/packages/desktop-electron/src/renderer/i18n/ja.ts b/packages/desktop-electron/src/renderer/i18n/ja.ts deleted file mode 100644 index fc485c6f401..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/ja.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "アップデートを確認...", - "desktop.menu.installCli": "CLI をインストール...", - "desktop.menu.reloadWebview": "Webview を再読み込み", - "desktop.menu.restart": "再起動", - - "desktop.dialog.chooseFolder": "フォルダーを選択", - "desktop.dialog.chooseFile": "ファイルを選択", - "desktop.dialog.saveFile": "ファイルを保存", - - "desktop.updater.checkFailed.title": "アップデートの確認に失敗しました", - "desktop.updater.checkFailed.message": "アップデートを確認できませんでした", - "desktop.updater.none.title": "利用可能なアップデートはありません", - "desktop.updater.none.message": "すでに最新バージョンの OpenCode を使用しています", - "desktop.updater.downloadFailed.title": "アップデートに失敗しました", - "desktop.updater.downloadFailed.message": "アップデートをダウンロードできませんでした", - "desktop.updater.downloaded.title": "アップデートをダウンロードしました", - "desktop.updater.downloaded.prompt": - "OpenCode のバージョン {{version}} がダウンロードされました。インストールして再起動しますか?", - "desktop.updater.installFailed.title": "アップデートに失敗しました", - "desktop.updater.installFailed.message": "アップデートをインストールできませんでした", - - "desktop.cli.installed.title": "CLI をインストールしました", - "desktop.cli.installed.message": - "CLI を {{path}} にインストールしました\n\nターミナルを再起動して 'opencode' コマンドを使用してください。", - "desktop.cli.failed.title": "インストールに失敗しました", - "desktop.cli.failed.message": "CLI のインストールに失敗しました: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/ko.ts b/packages/desktop-electron/src/renderer/i18n/ko.ts deleted file mode 100644 index be27cec86ac..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/ko.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "업데이트 확인...", - "desktop.menu.installCli": "CLI 설치...", - "desktop.menu.reloadWebview": "Webview 새로고침", - "desktop.menu.restart": "다시 시작", - - "desktop.dialog.chooseFolder": "폴더 선택", - "desktop.dialog.chooseFile": "파일 선택", - "desktop.dialog.saveFile": "파일 저장", - - "desktop.updater.checkFailed.title": "업데이트 확인 실패", - "desktop.updater.checkFailed.message": "업데이트를 확인하지 못했습니다", - "desktop.updater.none.title": "사용 가능한 업데이트 없음", - "desktop.updater.none.message": "이미 최신 버전의 OpenCode를 사용하고 있습니다", - "desktop.updater.downloadFailed.title": "업데이트 실패", - "desktop.updater.downloadFailed.message": "업데이트를 다운로드하지 못했습니다", - "desktop.updater.downloaded.title": "업데이트 다운로드 완료", - "desktop.updater.downloaded.prompt": "OpenCode {{version}} 버전을 다운로드했습니다. 설치하고 다시 실행할까요?", - "desktop.updater.installFailed.title": "업데이트 실패", - "desktop.updater.installFailed.message": "업데이트를 설치하지 못했습니다", - - "desktop.cli.installed.title": "CLI 설치됨", - "desktop.cli.installed.message": - "CLI가 {{path}}에 설치되었습니다\n\n터미널을 다시 시작하여 'opencode' 명령을 사용하세요.", - "desktop.cli.failed.title": "설치 실패", - "desktop.cli.failed.message": "CLI 설치 실패: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/nl.ts b/packages/desktop-electron/src/renderer/i18n/nl.ts deleted file mode 100644 index 2f8ca9fd423..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/nl.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Controleren op updates...", - "desktop.menu.installCli": "CLI installeren...", - "desktop.menu.reloadWebview": "Webview herladen", - "desktop.menu.restart": "Opnieuw opstarten", - - "desktop.dialog.chooseFolder": "Kies een map", - "desktop.dialog.chooseFile": "Kies een bestand", - "desktop.dialog.saveFile": "Bestand opslaan", - - "desktop.updater.checkFailed.title": "Controleren op updates mislukt", - "desktop.updater.checkFailed.message": "Kan niet controleren op updates", - "desktop.updater.none.title": "Geen update beschikbaar", - "desktop.updater.none.message": "Je gebruikt al de nieuwste versie van OpenCode", - "desktop.updater.downloadFailed.title": "Update mislukt", - "desktop.updater.downloadFailed.message": "Downloaden van update mislukt", - "desktop.updater.downloaded.title": "Update gedownload", - "desktop.updater.downloaded.prompt": - "Versie {{version}} van OpenCode is gedownload. Wil je deze installeren en opnieuw opstarten?", - "desktop.updater.installFailed.title": "Update mislukt", - "desktop.updater.installFailed.message": "Installeren van update mislukt", - - "desktop.cli.installed.title": "CLI geïnstalleerd", - "desktop.cli.installed.message": - "CLI geïnstalleerd in {{path}}\n\nHerstart je terminal om de opdracht 'opencode' te gebruiken.", - "desktop.cli.failed.title": "Installatie mislukt", - "desktop.cli.failed.message": "Installeren van CLI mislukt: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/no.ts b/packages/desktop-electron/src/renderer/i18n/no.ts deleted file mode 100644 index e39bd7f3b44..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/no.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Se etter oppdateringer...", - "desktop.menu.installCli": "Installer CLI...", - "desktop.menu.reloadWebview": "Last inn Webview på nytt", - "desktop.menu.restart": "Start på nytt", - - "desktop.dialog.chooseFolder": "Velg en mappe", - "desktop.dialog.chooseFile": "Velg en fil", - "desktop.dialog.saveFile": "Lagre fil", - - "desktop.updater.checkFailed.title": "Oppdateringssjekk mislyktes", - "desktop.updater.checkFailed.message": "Kunne ikke se etter oppdateringer", - "desktop.updater.none.title": "Ingen oppdatering tilgjengelig", - "desktop.updater.none.message": "Du bruker allerede den nyeste versjonen av OpenCode", - "desktop.updater.downloadFailed.title": "Oppdatering mislyktes", - "desktop.updater.downloadFailed.message": "Kunne ikke laste ned oppdateringen", - "desktop.updater.downloaded.title": "Oppdatering lastet ned", - "desktop.updater.downloaded.prompt": - "Versjon {{version}} av OpenCode er lastet ned. Vil du installere den og starte på nytt?", - "desktop.updater.installFailed.title": "Oppdatering mislyktes", - "desktop.updater.installFailed.message": "Kunne ikke installere oppdateringen", - - "desktop.cli.installed.title": "CLI installert", - "desktop.cli.installed.message": - "CLI installert til {{path}}\n\nStart terminalen på nytt for å bruke 'opencode'-kommandoen.", - "desktop.cli.failed.title": "Installasjon mislyktes", - "desktop.cli.failed.message": "Kunne ikke installere CLI: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/pl.ts b/packages/desktop-electron/src/renderer/i18n/pl.ts deleted file mode 100644 index d3ad7ce64f8..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/pl.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Sprawdź aktualizacje...", - "desktop.menu.installCli": "Zainstaluj CLI...", - "desktop.menu.reloadWebview": "Przeładuj Webview", - "desktop.menu.restart": "Restartuj", - - "desktop.dialog.chooseFolder": "Wybierz folder", - "desktop.dialog.chooseFile": "Wybierz plik", - "desktop.dialog.saveFile": "Zapisz plik", - - "desktop.updater.checkFailed.title": "Nie udało się sprawdzić aktualizacji", - "desktop.updater.checkFailed.message": "Nie udało się sprawdzić aktualizacji", - "desktop.updater.none.title": "Brak dostępnych aktualizacji", - "desktop.updater.none.message": "Korzystasz już z najnowszej wersji OpenCode", - "desktop.updater.downloadFailed.title": "Aktualizacja nie powiodła się", - "desktop.updater.downloadFailed.message": "Nie udało się pobrać aktualizacji", - "desktop.updater.downloaded.title": "Aktualizacja pobrana", - "desktop.updater.downloaded.prompt": - "Pobrano wersję {{version}} OpenCode. Czy chcesz ją zainstalować i uruchomić ponownie?", - "desktop.updater.installFailed.title": "Aktualizacja nie powiodła się", - "desktop.updater.installFailed.message": "Nie udało się zainstalować aktualizacji", - - "desktop.cli.installed.title": "CLI zainstalowane", - "desktop.cli.installed.message": - "CLI zainstalowane w {{path}}\n\nUruchom ponownie terminal, aby użyć polecenia 'opencode'.", - "desktop.cli.failed.title": "Instalacja nie powiodła się", - "desktop.cli.failed.message": "Nie udało się zainstalować CLI: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/ru.ts b/packages/desktop-electron/src/renderer/i18n/ru.ts deleted file mode 100644 index 8e09cc45b49..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/ru.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "Проверить обновления...", - "desktop.menu.installCli": "Установить CLI...", - "desktop.menu.reloadWebview": "Перезагрузить Webview", - "desktop.menu.restart": "Перезапустить", - - "desktop.dialog.chooseFolder": "Выберите папку", - "desktop.dialog.chooseFile": "Выберите файл", - "desktop.dialog.saveFile": "Сохранить файл", - - "desktop.updater.checkFailed.title": "Не удалось проверить обновления", - "desktop.updater.checkFailed.message": "Не удалось проверить обновления", - "desktop.updater.none.title": "Обновлений нет", - "desktop.updater.none.message": "Вы уже используете последнюю версию OpenCode", - "desktop.updater.downloadFailed.title": "Обновление не удалось", - "desktop.updater.downloadFailed.message": "Не удалось скачать обновление", - "desktop.updater.downloaded.title": "Обновление загружено", - "desktop.updater.downloaded.prompt": "Версия OpenCode {{version}} загружена. Хотите установить и перезапустить?", - "desktop.updater.installFailed.title": "Обновление не удалось", - "desktop.updater.installFailed.message": "Не удалось установить обновление", - - "desktop.cli.installed.title": "CLI установлен", - "desktop.cli.installed.message": - "CLI установлен в {{path}}\n\nПерезапустите терминал, чтобы использовать команду 'opencode'.", - "desktop.cli.failed.title": "Ошибка установки", - "desktop.cli.failed.message": "Не удалось установить CLI: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/zh.ts b/packages/desktop-electron/src/renderer/i18n/zh.ts deleted file mode 100644 index aeb3a54e03e..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/zh.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "检查更新...", - "desktop.menu.installCli": "安装 CLI...", - "desktop.menu.reloadWebview": "重新加载 Webview", - "desktop.menu.restart": "重启", - - "desktop.dialog.chooseFolder": "选择文件夹", - "desktop.dialog.chooseFile": "选择文件", - "desktop.dialog.saveFile": "保存文件", - - "desktop.updater.checkFailed.title": "检查更新失败", - "desktop.updater.checkFailed.message": "无法检查更新", - "desktop.updater.none.title": "没有可用更新", - "desktop.updater.none.message": "你已经在使用最新版本的 OpenCode", - "desktop.updater.downloadFailed.title": "更新失败", - "desktop.updater.downloadFailed.message": "无法下载更新", - "desktop.updater.downloaded.title": "更新已下载", - "desktop.updater.downloaded.prompt": "已下载 OpenCode {{version}} 版本,是否安装并重启?", - "desktop.updater.installFailed.title": "更新失败", - "desktop.updater.installFailed.message": "无法安装更新", - - "desktop.cli.installed.title": "CLI 已安装", - "desktop.cli.installed.message": "CLI 已安装到 {{path}}\n\n重启终端以使用 'opencode' 命令。", - "desktop.cli.failed.title": "安装失败", - "desktop.cli.failed.message": "无法安装 CLI: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/i18n/zht.ts b/packages/desktop-electron/src/renderer/i18n/zht.ts deleted file mode 100644 index 7fd677aca44..00000000000 --- a/packages/desktop-electron/src/renderer/i18n/zht.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const dict = { - "desktop.menu.checkForUpdates": "檢查更新...", - "desktop.menu.installCli": "安裝 CLI...", - "desktop.menu.reloadWebview": "重新載入 Webview", - "desktop.menu.restart": "重新啟動", - - "desktop.dialog.chooseFolder": "選擇資料夾", - "desktop.dialog.chooseFile": "選擇檔案", - "desktop.dialog.saveFile": "儲存檔案", - - "desktop.updater.checkFailed.title": "檢查更新失敗", - "desktop.updater.checkFailed.message": "無法檢查更新", - "desktop.updater.none.title": "沒有可用更新", - "desktop.updater.none.message": "你已在使用最新版的 OpenCode", - "desktop.updater.downloadFailed.title": "更新失敗", - "desktop.updater.downloadFailed.message": "無法下載更新", - "desktop.updater.downloaded.title": "更新已下載", - "desktop.updater.downloaded.prompt": "已下載 OpenCode {{version}} 版本,是否安裝並重新啟動?", - "desktop.updater.installFailed.title": "更新失敗", - "desktop.updater.installFailed.message": "無法安裝更新", - - "desktop.cli.installed.title": "CLI 已安裝", - "desktop.cli.installed.message": "CLI 已安裝到 {{path}}\n\n重新啟動終端機以使用 'opencode' 命令。", - "desktop.cli.failed.title": "安裝失敗", - "desktop.cli.failed.message": "無法安裝 CLI: {{error}}", -} diff --git a/packages/desktop-electron/src/renderer/index.html b/packages/desktop-electron/src/renderer/index.html deleted file mode 100644 index dd8675ee6b9..00000000000 --- a/packages/desktop-electron/src/renderer/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - OpenCode - - - - - - - - - - - - -
- - - diff --git a/packages/desktop-electron/src/renderer/index.tsx b/packages/desktop-electron/src/renderer/index.tsx deleted file mode 100644 index a1a11adf198..00000000000 --- a/packages/desktop-electron/src/renderer/index.tsx +++ /dev/null @@ -1,357 +0,0 @@ -// @refresh reload - -import { - ACCEPTED_FILE_EXTENSIONS, - ACCEPTED_FILE_TYPES, - AppBaseProviders, - AppInterface, - handleNotificationClick, - loadLocaleDict, - normalizeLocale, - type Locale, - type Platform, - PlatformProvider, - ServerConnection, - useCommand, -} from "@opencode-ai/app" -import type { AsyncStorage } from "@solid-primitives/storage" -import { MemoryRouter } from "@solidjs/router" -import { createEffect, createResource, onCleanup, onMount, Show } from "solid-js" -import { render } from "solid-js/web" -import pkg from "../../package.json" -import { initI18n, t } from "./i18n" -import { webviewZoom } from "./webview-zoom" -import "./styles.css" -import { useTheme } from "@opencode-ai/ui/theme" - -const root = document.getElementById("root") -if (import.meta.env.DEV && !(root instanceof HTMLElement)) { - throw new Error(t("error.dev.rootNotFound")) -} - -void initI18n() - -const deepLinkEvent = "opencode:deep-link" - -const emitDeepLinks = (urls: string[]) => { - if (urls.length === 0) return - window.__KILO__ ??= {} - const pending = window.__KILO__.deepLinks ?? [] - window.__KILO__.deepLinks = [...pending, ...urls] - window.dispatchEvent(new CustomEvent(deepLinkEvent, { detail: { urls } })) -} - -const listenForDeepLinks = () => { - void window.api.consumeInitialDeepLinks().then((urls) => emitDeepLinks(urls)) - return window.api.onDeepLink((urls) => emitDeepLinks(urls)) -} - -const createPlatform = (): Platform => { - const os = (() => { - const ua = navigator.userAgent - if (ua.includes("Mac")) return "macos" - if (ua.includes("Windows")) return "windows" - if (ua.includes("Linux")) return "linux" - return undefined - })() - - const isWslEnabled = async () => { - if (os !== "windows") return false - return window.api - .getWslConfig() - .then((config) => config.enabled) - .catch(() => false) - } - - const wslHome = async () => { - if (!(await isWslEnabled())) return undefined - return window.api.wslPath("~", "windows").catch(() => undefined) - } - - const handleWslPicker = async (result: T | null): Promise => { - if (!result || !(await isWslEnabled())) return result - if (Array.isArray(result)) { - return Promise.all(result.map((path) => window.api.wslPath(path, "linux").catch(() => path))) as any - } - return window.api.wslPath(result, "linux").catch(() => result) as any - } - - const storage = (() => { - const cache = new Map() - - const createStorage = (name: string) => { - const api: AsyncStorage = { - getItem: (key: string) => window.api.storeGet(name, key), - setItem: (key: string, value: string) => window.api.storeSet(name, key, value), - removeItem: (key: string) => window.api.storeDelete(name, key), - clear: () => window.api.storeClear(name), - key: async (index: number) => (await window.api.storeKeys(name))[index], - getLength: () => window.api.storeLength(name), - get length() { - return api.getLength() - }, - } - return api - } - - return (name = "default.dat") => { - const cached = cache.get(name) - if (cached) return cached - const api = createStorage(name) - cache.set(name, api) - return api - } - })() - - return { - platform: "desktop", - os, - version: pkg.version, - - async openDirectoryPickerDialog(opts) { - const defaultPath = await wslHome() - const result = await window.api.openDirectoryPicker({ - multiple: opts?.multiple ?? false, - title: opts?.title ?? t("desktop.dialog.chooseFolder"), - defaultPath, - }) - return await handleWslPicker(result) - }, - - async openFilePickerDialog(opts) { - const result = await window.api.openFilePicker({ - multiple: opts?.multiple ?? false, - title: opts?.title ?? t("desktop.dialog.chooseFile"), - accept: opts?.accept ?? ACCEPTED_FILE_TYPES, - extensions: opts?.extensions ?? ACCEPTED_FILE_EXTENSIONS, - }) - return handleWslPicker(result) - }, - - async saveFilePickerDialog(opts) { - const result = await window.api.saveFilePicker({ - title: opts?.title ?? t("desktop.dialog.saveFile"), - defaultPath: opts?.defaultPath, - }) - return handleWslPicker(result) - }, - - openLink(url: string) { - window.api.openLink(url) - }, - async openPath(path: string, app?: string) { - if (os === "windows") { - const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null - const resolvedPath = await (async () => { - if (await isWslEnabled()) { - const converted = await window.api.wslPath(path, "windows").catch(() => null) - if (converted) return converted - } - return path - })() - return window.api.openPath(resolvedPath, resolvedApp ?? undefined) - } - return window.api.openPath(path, app) - }, - - back() { - window.history.back() - }, - - forward() { - window.history.forward() - }, - - storage, - - checkUpdate: async () => { - const config = await window.api.getWindowConfig().catch(() => ({ updaterEnabled: false })) - if (!config.updaterEnabled) return { updateAvailable: false } - return window.api.checkUpdate() - }, - - updateAndRestart: async () => { - const config = await window.api.getWindowConfig().catch(() => ({ updaterEnabled: false })) - if (!config.updaterEnabled) return - await window.api.installUpdate() - }, - - restart: async () => { - await window.api.killSidecar().catch(() => undefined) - window.api.relaunch() - }, - - notify: async (title, description, href) => { - const focused = await window.api.getWindowFocused().catch(() => document.hasFocus()) - if (focused) return - - const notification = new Notification(title, { - body: description ?? "", - icon: "https://opencode.ai/favicon-96x96-v3.png", - }) - notification.onclick = () => { - void window.api.showWindow() - void window.api.setWindowFocus() - handleNotificationClick(href) - notification.close() - } - }, - - fetch: (input, init) => { - if (input instanceof Request) return fetch(input) - return fetch(input, init) - }, - - getWslEnabled: () => isWslEnabled(), - - setWslEnabled: async (enabled) => { - await window.api.setWslConfig({ enabled }) - }, - - getDefaultServer: async () => { - const url = await window.api.getDefaultServerUrl().catch(() => null) - if (!url) return null - return ServerConnection.Key.make(url) - }, - - setDefaultServer: async (url: string | null) => { - await window.api.setDefaultServerUrl(url) - }, - - getDisplayBackend: async () => { - return window.api.getDisplayBackend().catch(() => null) - }, - - setDisplayBackend: async (backend) => { - await window.api.setDisplayBackend(backend) - }, - - parseMarkdown: (markdown: string) => window.api.parseMarkdownCommand(markdown), - - webviewZoom, - - checkAppExists: async (appName: string) => { - return window.api.checkAppExists(appName) - }, - - async readClipboardImage() { - const image = await window.api.readClipboardImage().catch(() => null) - if (!image) return null - const blob = new Blob([image.buffer], { type: "image/png" }) - return new File([blob], `pasted-image-${Date.now()}.png`, { - type: "image/png", - }) - }, - } -} - -let menuTrigger = null as null | ((id: string) => void) -window.api.onMenuCommand((id) => { - menuTrigger?.(id) -}) -listenForDeepLinks() - -render(() => { - const platform = createPlatform() - const [windowConfig] = createResource(() => window.api.getWindowConfig().catch(() => ({ updaterEnabled: false }))) - const loadLocale = async () => { - const current = await platform.storage?.("opencode.global.dat").getItem("language") - const legacy = current ? undefined : await platform.storage?.().getItem("language.v1") - const raw = current ?? legacy - if (!raw) return - const locale = raw.match(/"locale"\s*:\s*"([^"]+)"/)?.[1] - if (!locale) return - const next = normalizeLocale(locale) - if (next !== "en") await loadLocaleDict(next) - return next satisfies Locale - } - - const [windowCount] = createResource(() => window.api.getWindowCount()) - - // Fetch sidecar credentials (available immediately, before health check) - const [sidecar] = createResource(() => window.api.awaitInitialization(() => undefined)) - - const [defaultServer] = createResource(() => - platform.getDefaultServer?.().then((url) => { - if (url) return ServerConnection.key({ type: "http", http: { url } }) - }), - ) - const [locale] = createResource(loadLocale) - - const servers = () => { - const data = sidecar() - if (!data) return [] - const server: ServerConnection.Sidecar = { - displayName: "Local Server", - type: "sidecar", - variant: "base", - http: { - url: data.url, - username: data.username ?? undefined, - password: data.password ?? undefined, - }, - } - return [server] as ServerConnection.Any[] - } - - function handleClick(e: MouseEvent) { - const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null - if (link?.href) { - e.preventDefault() - platform.openLink(link.href) - } - } - - function Inner() { - const cmd = useCommand() - menuTrigger = (id) => cmd.trigger(id) - - const theme = useTheme() - - createEffect(() => { - theme.themeId() - theme.mode() - const bg = getComputedStyle(document.documentElement).getPropertyValue("--background-base").trim() - if (bg) { - void window.api.setBackgroundColor(bg) - } - }) - - return null - } - - onMount(() => { - document.addEventListener("click", handleClick) - onCleanup(() => { - document.removeEventListener("click", handleClick) - }) - }) - - return ( - - - - {(_) => { - return ( - - - - ) - }} - - - - ) -}, root!) diff --git a/packages/desktop-electron/src/renderer/loading.html b/packages/desktop-electron/src/renderer/loading.html deleted file mode 100644 index ae3725af61e..00000000000 --- a/packages/desktop-electron/src/renderer/loading.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - OpenCode - - - - - - - - - - - - -
- - - diff --git a/packages/desktop-electron/src/renderer/loading.tsx b/packages/desktop-electron/src/renderer/loading.tsx deleted file mode 100644 index 000057e0a87..00000000000 --- a/packages/desktop-electron/src/renderer/loading.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { MetaProvider } from "@solidjs/meta" -import { render } from "solid-js/web" -import "@opencode-ai/app/index.css" -import { Font } from "@opencode-ai/ui/font" -import { Splash } from "@opencode-ai/ui/logo" -import { Progress } from "@opencode-ai/ui/progress" -import "./styles.css" -import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js" -import type { InitStep, SqliteMigrationProgress } from "../preload/types" - -const root = document.getElementById("root")! -const lines = ["Just a moment...", "Migrating your database", "This may take a couple of minutes"] -const delays = [3000, 9000] - -render(() => { - const [step, setStep] = createSignal(null) - const [line, setLine] = createSignal(0) - const [percent, setPercent] = createSignal(0) - - const phase = createMemo(() => step()?.phase) - - const value = createMemo(() => { - if (phase() === "done") return 100 - return Math.max(25, Math.min(100, percent())) - }) - - window.api.awaitInitialization((next) => setStep(next as InitStep)).catch(() => undefined) - - onMount(() => { - setLine(0) - setPercent(0) - - const timers = delays.map((ms, i) => setTimeout(() => setLine(i + 1), ms)) - - const listener = window.api.onSqliteMigrationProgress((progress: SqliteMigrationProgress) => { - if (progress.type === "InProgress") setPercent(Math.max(0, Math.min(100, progress.value))) - if (progress.type === "Done") { - setPercent(100) - setStep({ phase: "done" }) - } - }) - - onCleanup(() => { - listener() - timers.forEach(clearTimeout) - }) - }) - - createEffect(() => { - if (phase() !== "done") return - - const timer = setTimeout(() => window.api.loadingWindowComplete(), 1000) - onCleanup(() => clearTimeout(timer)) - }) - - const status = createMemo(() => { - if (phase() === "done") return "All done" - if (phase() === "sqlite_waiting") return lines[line()] - return "Just a moment..." - }) - - return ( - -
- -
- -
- - {status()} - - `${Math.round(value)}%`} - /> -
-
-
-
- ) -}, root) diff --git a/packages/desktop-electron/src/renderer/styles.css b/packages/desktop-electron/src/renderer/styles.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/desktop-electron/src/renderer/updater.ts b/packages/desktop-electron/src/renderer/updater.ts deleted file mode 100644 index c44ca26591d..00000000000 --- a/packages/desktop-electron/src/renderer/updater.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { initI18n, t } from "./i18n" - -export async function runUpdater({ alertOnFail }: { alertOnFail: boolean }) { - await initI18n() - try { - await window.api.runUpdater(alertOnFail) - } catch { - if (alertOnFail) { - window.alert(t("desktop.updater.checkFailed.message")) - } - } -} diff --git a/packages/desktop-electron/src/renderer/webview-zoom.ts b/packages/desktop-electron/src/renderer/webview-zoom.ts deleted file mode 100644 index 9c0a3a3a35f..00000000000 --- a/packages/desktop-electron/src/renderer/webview-zoom.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2019-2024 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -import { createSignal } from "solid-js" - -const OS_NAME = (() => { - if (navigator.userAgent.includes("Mac")) return "macos" - if (navigator.userAgent.includes("Windows")) return "windows" - if (navigator.userAgent.includes("Linux")) return "linux" - return "unknown" -})() - -const [webviewZoom, setWebviewZoom] = createSignal(1) - -const MAX_ZOOM_LEVEL = 10 -const MIN_ZOOM_LEVEL = 0.2 - -const clamp = (value: number) => Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL) - -const applyZoom = (next: number) => { - setWebviewZoom(next) - void window.api.setZoomFactor(next) -} - -window.addEventListener("keydown", (event) => { - if (!(OS_NAME === "macos" ? event.metaKey : event.ctrlKey)) return - - let newZoom = webviewZoom() - - if (event.key === "-") newZoom -= 0.2 - if (event.key === "=" || event.key === "+") newZoom += 0.2 - if (event.key === "0") newZoom = 1 - - applyZoom(clamp(newZoom)) -}) - -export { webviewZoom } diff --git a/packages/desktop-electron/sst-env.d.ts b/packages/desktop-electron/sst-env.d.ts deleted file mode 100644 index f25b9714550..00000000000 --- a/packages/desktop-electron/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/desktop-electron/tsconfig.json b/packages/desktop-electron/tsconfig.json deleted file mode 100644 index 9637fe03ddc..00000000000 --- a/packages/desktop-electron/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "jsx": "preserve", - "jsxImportSource": "solid-js", - "allowJs": true, - "resolveJsonModule": true, - "strict": true, - "isolatedModules": true, - "noEmit": true, - "emitDeclarationOnly": false, - "outDir": "node_modules/.ts-dist", - "types": ["vite/client", "node", "electron"] - }, - "references": [{ "path": "../app" }], - "include": ["src", "package.json"], - "exclude": ["src/**/*.test.ts"] -} diff --git a/packages/desktop/.gitignore b/packages/desktop/.gitignore deleted file mode 100644 index a547bf36d8d..00000000000 --- a/packages/desktop/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/packages/desktop/README.md b/packages/desktop/README.md deleted file mode 100644 index 358b7d24d51..00000000000 --- a/packages/desktop/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# OpenCode Desktop - -Native OpenCode desktop app, built with Tauri v2. - -## Prerequisites - -Building the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions. - -## Development - -From the repo root: - -```bash -bun install -bun run --cwd packages/desktop tauri dev -``` - -## Build - -```bash -bun run --cwd packages/desktop tauri build -``` - -## Troubleshooting - -### Rust compiler not found - -If you see errors about Rust not being found, install it via [rustup](https://rustup.rs/): - -```bash -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -``` diff --git a/packages/desktop/index.html b/packages/desktop/index.html deleted file mode 100644 index ce2775a7047..00000000000 --- a/packages/desktop/index.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - OpenCode - - - - - - - - - - - - - -
-